Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions .github/scripts/bot-workflows.sh
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +18 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Quote the variable in the error message.

Line 25 should quote $DRY_RUN to prevent word splitting and ensure proper error output.

🔧 Proposed fix
-    echo "ERROR: DRY_RUN must be one of: true, false, 1, 0 (got: $DRY_RUN)"
+    echo "ERROR: DRY_RUN must be one of: true, false, 1, 0 (got: '$DRY_RUN')"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 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
# 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
Comment on lines +67 to +70
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider validating REPO format.

The script assumes REPO follows the owner/repo format required by GitHub CLI commands (lines 103, 118, 158, 197). Without validation, malformed input could cause obscure failures later in the script.

🔍 Proposed format validation
 if [[ -z "$REPO" ]]; then
   echo "ERROR: REPO environment variable not set."
   exit 1
 fi
+
+# Validate REPO format (must be owner/repo)
+if ! [[ "$REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
+  echo "ERROR: REPO must be in 'owner/repo' format (got: '$REPO')"
+  exit 1
+fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [[ -z "$REPO" ]]; then
echo "ERROR: REPO environment variable not set."
exit 1
fi
if [[ -z "$REPO" ]]; then
echo "ERROR: REPO environment variable not set."
exit 1
fi
# Validate REPO format (must be owner/repo)
if ! [[ "$REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
echo "ERROR: REPO must be in 'owner/repo' format (got: '$REPO')"
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."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are refactoring, i would recommend to use javascript and github action, will simplify a lot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want me to completely change it into js and github action or only the particular thing and sort of hybrid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All as JS is my suggestion - not essential - but if you are refactoring, this will typically lead to better code

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="<!-- workflowbot:workflow-failure-notifier -->"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be at the top so we know we can change it

COMMENT=$(cat <<EOF
$MARKER
Hi, this is WorkflowBot.
Your pull request cannot be merged as it is not passing all our workflow checks.
Please click on each check to review the logs and resolve issues so all checks pass.
To help you:
- [DCO signing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move these links to the top so we can edit them later easily, rather than have them hard coded in the script

- [Changelog guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md)
- [Merge conflicts guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md)
- [Rebase guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md)
- [Testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md)
- [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md)
- [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week)
Thank you for contributing!
From the Hiero Python SDK Team
EOF
)

# Check for duplicate comments using the correct endpoint for issue comments
PAGE=1
DUPLICATE_EXISTS="false"
MAX_PAGES=10 # Safety bound

while (( PAGE <= MAX_PAGES )); do
COMMENTS_PAGE=$(gh api \
--header 'Accept: application/vnd.github.v3+json' \
"/repos/$REPO/issues/$PR_NUMBER/comments?per_page=100&page=$PAGE" 2>/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 "------------------------------------------------------------"
67 changes: 24 additions & 43 deletions .github/workflows/bot-workflows.yml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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' }}
Comment on lines +30 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider simplifying the DRY_RUN expression for readability.

The nested ternary expression is correct but dense:

DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.dry_run == 'true' && '1' || '0') || '0' }}

While functionally sound, a comment explaining the logic would improve maintainability.

📝 Add clarifying comment
     env:
-      # Behaviour:
-      # - workflow_run: DRY_RUN = 0 (real actions)
-      # - workflow_dispatch: DRY_RUN derived from the "dry_run" input
+      # DRY_RUN behavior by event type:
+      #   - workflow_run: always 0 (real actions - post comments)
+      #   - workflow_dispatch with dry_run=true: 1 (simulate only)
+      #   - workflow_dispatch with dry_run=false: 0 (real actions)
       DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.dry_run == 'true' && '1' || '0') || '0' }}

steps:
Comment on lines +30 to 35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

git ls-files .github/workflows/ | head -20

Repository: hiero-ledger/hiero-sdk-python

Length of output: 1026


🏁 Script executed:

fd "bot-workflows" .github/

Repository: hiero-ledger/hiero-sdk-python

Length of output: 141


🏁 Script executed:

cat -n .github/workflows/bot-workflows.yml | head -50

Repository: hiero-ledger/hiero-sdk-python

Length of output: 2271


🌐 Web query:

GitHub Actions workflow_dispatch boolean input type github.event.inputs

💡 Result:

Short answer: github.event.inputs values are strings (so boolean inputs arrive as 'true'/'false'). Use the inputs context (inputs.) to get preserved Boolean values, or explicitly convert/compare (e.g. github.event.inputs.foo == 'true' or fromJson) when you must read github.event.inputs. [1][2][3][4]

References

  • Workflow docs: inputs context preserves booleans; github.event.inputs is stringy. [1][2]
  • Changelog (input types added). [3]
  • Community/issue discussion and recommended workarounds (compare to 'true' / fromJson). [4][5]

🌐 Web query:

GitHub Actions inputs vs github.event.inputs workflow_dispatch

💡 Result:

Short answer

  • Use inputs (e.g. ${{ inputs.foo }}) — it's the unified, preferred context for values defined in on.workflow_dispatch and on.workflow_call. It preserves boolean/number types for workflow_dispatch and works for reusable workflows. [1][2][3][4]

  • github.event.inputs (e.g. ${{ github.event.inputs.foo }}) is the older event-payload location kept for backward compatibility; values there are (historically) stringified for workflow_dispatch. Prefer inputs when possible. [1][2][3]

Quick example

  • workflow file:
    on:
    workflow_dispatch:
    inputs:
    flag:
    type: boolean
  • access:
    • ${{ inputs.flag }} # boolean
    • ${{ github.event.inputs.flag }} # kept for compatibility (may be stringified). [1][2][4]

Sources
[1] GitHub Docs — Triggering a workflow (workflow_dispatch inputs note).
[2] GitHub Docs — Workflow syntax (on.workflow_dispatch, inputs note).
[3] GitHub changelog — Inputs unified across manual and reusable workflows (June 2022).
[4] GitHub Docs — Contexts reference (inputs context).


Use the inputs context instead of github.event.inputs for boolean input access.

For workflow_dispatch with a typed boolean input, github.event.inputs.dry_run is a string ('true'/'false'), not a boolean. The inputs context preserves the actual boolean type and is the preferred way to access workflow inputs. This simplifies the expression while maintaining the 1/0 contract for the script.

Proposed fix
 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' }}
+  DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && (inputs.dry_run && '1' || '0') || '0' }}

- 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 <<EOF
Hi, this is WorkflowBot.
Your pull request cannot be merged as it is not passing all our workflow checks.
Please click on each check to review the logs and resolve issues so all checks pass.
To help you:
- [DCO signing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md)
- [Changelog guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md)
- [Merge conflicts guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md)
- [Rebase guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md)
- [Testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md)
- [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md)
- [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week)
Thank you for contributing!
From the Hiero Python SDK Team
EOF
)
EXISTING_COMMENT=$(gh pr view "$PR_NUMBER" --repo "$REPO" --comments \
--json comments --jq ".comments[] | select(.body == \"${COMMENT//\"/\\\"}\") | .id")

if [ -z "$EXISTING_COMMENT" ]; then
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$COMMENT"
else
echo "Comment already exists, skipping."
fi
REPO: ${{ github.repository }}
DRY_RUN: ${{ env.DRY_RUN }}
run: bash .github/scripts/bot-workflows.sh
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Add GitHub Actions script and workflow for automatic spam list updates.
- Added technical docstrings and hardening (set -euo pipefail) to the pr-check-test-files.sh script (#1336)
- Added prompt for coderabbit to review `Query` and it's sub-classes.

- Added dry-run support and refactored `bot-workflows.yml` to use dedicated script `.github/scripts/bot-workflows.sh` for improved maintainability and testability. (#1288)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prefer the full workflow path in the changelog entry for clarity.

Consider referencing .github/workflows/bot-workflows.yml (not just bot-workflows.yml) so readers can locate it unambiguously.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~99-~99: The official name of this software platform is spelled with a capital “H”.
Context: ...t-workflows.ymlto use dedicated script.github/scripts/bot-workflows.sh` for improved ...

(GITHUB)

### Changed
- Remove the commented out blocks in config.yml (#1435)
- Renamed `.github/scripts/check_advanced_requirement.sh` to `bot-advanced-check.sh` for workflow consistency (#1341)
Expand Down