Repository navigation
Update Mattermost PR Dashboard #4236
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update Mattermost PR Dashboard | |
| on: | |
| schedule: | |
| # Runs every 5 minutes (may be delayed during high GitHub Actions load) | |
| - cron: '*/5 * * * *' | |
| workflow_dispatch: | |
| repository_dispatch: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| jobs: | |
| update-dashboard: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Update Mattermost Dashboard Post | |
| env: | |
| GH_TOKEN: ${{ secrets.ORG_READ_PAT }} | |
| MM_URL: ${{ secrets.MATTERMOST_URL }} | |
| MM_TOKEN: ${{ secrets.MATTERMOST_TOKEN }} | |
| POST_ID: ${{ secrets.MATTERMOST_POST_ID }} | |
| TARGET_ORGS: "acep-devops acep-aedg acep-uaf" | |
| run: | | |
| ORG_FILTER="" | |
| for org in $TARGET_ORGS; do | |
| ORG_FILTER="$ORG_FILTER org:$org" | |
| done | |
| QUERY="query { search(query: \"$ORG_FILTER is:pr is:open draft:false archived:false\", type: ISSUE, first: 50) { issueCount nodes { ... on PullRequest { number title url reviewDecision mergeable createdAt updatedAt author { login } repository { name } reviewRequests(first: 1) { totalCount } } } } }" | |
| export RAW_JSON=$(gh api graphql -f query="$QUERY") | |
| python3 -c ' | |
| import os, json, sys, urllib.request | |
| from datetime import datetime, timezone | |
| from zoneinfo import ZoneInfo | |
| raw_str = os.environ.get("RAW_JSON", "") | |
| if not raw_str: | |
| print("::error::RAW_JSON returned empty from GitHub API.") | |
| sys.exit(1) | |
| data = json.loads(raw_str) | |
| if "errors" in data: | |
| print("::error::GitHub API Error:", json.dumps(data["errors"])) | |
| sys.exit(1) | |
| search = data.get("data", {}).get("search", {}) | |
| nodes = search.get("nodes", []) | |
| total_count = search.get("issueCount", len(nodes)) | |
| if not nodes: | |
| message = "### Live PR Queue\n\n🎉 *No open PRs waiting for review!*" | |
| else: | |
| needs_review, changes_requested, approved = [], [], [] | |
| # Set up local timezone | |
| local_tz = ZoneInfo("America/Anchorage") | |
| now = datetime.now(local_tz) | |
| # Configuration for stale PRs | |
| STALE_THRESHOLD_DAYS = 14 | |
| for pr in nodes: | |
| repo = pr.get("repository", {}).get("name", "repo") | |
| num = pr.get("number") | |
| url = pr.get("url") | |
| title = pr.get("title", "") | |
| author = pr.get("author", {}).get("login", "unknown") if pr.get("author") else "unknown" | |
| decision = pr.get("reviewDecision") | |
| created_raw = pr.get("createdAt") | |
| mergeable = pr.get("mergeable", "UNKNOWN") | |
| review_requests_data = pr.get("reviewRequests") or {} | |
| review_requests_count = review_requests_data.get("totalCount", 0) | |
| days_old = 0 | |
| if created_raw: | |
| try: | |
| # Parse the UTC time from GitHub | |
| dt_utc = datetime.fromisoformat(created_raw.replace("Z", "+00:00")) | |
| # Convert to your local Anchorage time | |
| dt_local = dt_utc.astimezone(local_tz) | |
| # Compare just the calendar dates (Year-Month-Day) | |
| days_old = (now.date() - dt_local.date()).days | |
| days_old = max(0, days_old) # Prevent negative numbers just in case | |
| except Exception: | |
| pass | |
| if days_old == 0: | |
| time_str = "Today" | |
| elif days_old == 1: | |
| time_str = "Yesterday" | |
| else: | |
| time_str = f"{days_old}d ago" | |
| # Apply the stale warning based on our threshold variable | |
| stale = " ⚠️" if days_old >= STALE_THRESHOLD_DAYS else "" | |
| conflict = " 🚨 Resolve Merge Conflict" if mergeable == "CONFLICTING" else "" | |
| # Bot check | |
| is_bot = author.lower() in ["dependabot", "renovate", "github-actions", "dependabot[bot]"] | |
| bot_icon = " 🤖" if is_bot else "" | |
| # Clean title for tooltip ONLY | |
| safe_title = title.replace(chr(34), "").replace(chr(39), "") | |
| # Build the line: Link (with title in tooltip) -> Bot icon -> Author -> Conflict/Stale -> Days | |
| line = f"**[{repo}#{num}]({url} {chr(34)}{safe_title}{chr(34)})**{bot_icon} (*@ {author}*){conflict}{stale} `{time_str}`" | |
| if decision == "APPROVED": | |
| approved.append(line) | |
| elif decision == "CHANGES_REQUESTED": | |
| # If changes were requested, but a review is actively requested again, | |
| # we override and put it in "Needs Review" | |
| if review_requests_count > 0: | |
| needs_review.append(line) | |
| else: | |
| changes_requested.append(line) | |
| else: | |
| needs_review.append(line) | |
| message = f"### Open PRs ({total_count}) \n\n **Orgs: acep-devops, acep-uaf, acep-aedg**\n\n *Runs every 5 minutes (may be delayed during high GitHub Actions load) OR is instant when repo has the trigger-dashboard.yml action*\n\n *Hover over link to see PR title*\n\n ---\n" | |
| if approved: | |
| message += "#### ✅ Approved\n" + "\n".join(approved) + "\n\n" | |
| if changes_requested: | |
| message += "#### 🔁 Changes Requested\n" + "\n".join(changes_requested) + "\n\n" | |
| if needs_review: | |
| message += "#### 👀 Needs Review\n" + "\n".join(needs_review) + "\n\n" | |
| # Check if there are more PRs than what fit in the query response | |
| if total_count > len(nodes): | |
| more_count = total_count - len(nodes) | |
| message += f"*... and {more_count} more open PRs not shown*\n\n" | |
| refresh_time = now.strftime("%b %d, %I:%M %p %Z") | |
| message += f"---\n*🔄 Last updated: {refresh_time}*" | |
| mm_url = os.environ.get("MM_URL", "").rstrip("/") | |
| post_id = os.environ.get("POST_ID", "") | |
| mm_token = os.environ.get("MM_TOKEN", "") | |
| if not mm_url or not post_id or not mm_token: | |
| print("::error::Missing required environment variables.") | |
| sys.exit(1) | |
| endpoint = f"{mm_url}/api/v4/posts/{post_id}" | |
| payload = json.dumps({"id": post_id, "message": message}).encode("utf-8") | |
| req = urllib.request.Request( | |
| endpoint, | |
| data=payload, | |
| headers={"Authorization": f"Bearer {mm_token}", "Content-Type": "application/json"}, | |
| method="PUT" | |
| ) | |
| try: | |
| with urllib.request.urlopen(req) as resp: | |
| print(f"Successfully updated Mattermost! HTTP Status: {resp.status}") | |
| except Exception as e: | |
| print(f"::error::Failed to update Mattermost: {e}") | |
| sys.exit(1) | |
| ' |