PR Tracking and Attention Report #225
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: PR Tracking and Attention Report | |
| on: | |
| schedule: | |
| # Run daily at 9am UTC | |
| - cron: '0 9 * * *' | |
| workflow_dispatch: | |
| jobs: | |
| track-prs: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout .github repo | |
| uses: actions/checkout@v7 | |
| - name: Create or checkout pr-tracking branch | |
| run: | | |
| git fetch origin pr-tracking:pr-tracking 2>/dev/null || git checkout -b pr-tracking | |
| git checkout pr-tracking | |
| - name: Analyze PRs and Generate Reports | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| mkdir -p pr-tracking | |
| # Get team members (maintainers) | |
| # GITHUB_TOKEN doesn't have permission to read team members | |
| # Using hardcoded list of automation-proddev team members | |
| # Update this list when team membership changes | |
| team_members="gschueler jtobard ltamaster carlosrfranco fdevans jsboak ronaveva jayas006 Jesus-Osuna-M smartinellibenedetti gbhutani-pd" | |
| echo "Team members count: $(echo $team_members | wc -w)" | |
| echo "# PR Report - $(date +%Y-%m-%d)" > pr-count.md | |
| echo "" >> pr-count.md | |
| echo "## Summary" >> pr-count.md | |
| echo "" >> pr-count.md | |
| # Get all repos in the org | |
| repos=$(gh repo list rundeck-plugins --limit 100 --json name -q '.[].name') | |
| total_prs=0 | |
| repos_with_prs=0 | |
| total_issues=0 | |
| repos_with_issues=0 | |
| needs_attention=0 | |
| # Buffer rows and sort by open-issue count (descending) before | |
| # writing the table - the point of adding this column is to spot | |
| # repos with a growing issue backlog at a glance, so put the | |
| # noisiest ones first rather than whatever order gh repo list returns. | |
| > rows.tsv | |
| # Temp file for attention details | |
| > attention-details.md | |
| for repo in $repos; do | |
| # Check if repo is archived | |
| is_archived=$(gh repo view rundeck-plugins/$repo --json isArchived -q '.isArchived') | |
| if [ "$is_archived" = "true" ]; then | |
| continue | |
| fi | |
| # Count total PRs. `gh pr list` (not the raw /issues API, which | |
| # mixes PRs and issues together) so this stays PR-only. | |
| # --limit explicitly set well above gh's default of 30 - that | |
| # default silently truncated results (a real miss: it dropped | |
| # rundeck-ec2-nodes-plugin's oldest open issue, #27, from a | |
| # manual count run off this same pattern before this fix). | |
| count=$(gh pr list --repo rundeck-plugins/$repo --state open --json number --limit 500 -q 'length') | |
| # `gh issue list` is issue-only by construction, same reasoning | |
| # in reverse - it never counts open PRs as issues. Excludes | |
| # Renovate's "Dependency Dashboard" issue, which sits open in | |
| # every Renovate-enabled repo permanently and isn't a real | |
| # backlog signal. | |
| issue_count=$(gh issue list --repo rundeck-plugins/$repo --state open --json title --limit 500 -q '[.[] | select(.title != "Dependency Dashboard")] | length') | |
| if [ "$count" -eq 0 ] && [ "$issue_count" -eq 0 ]; then | |
| continue | |
| fi | |
| # Analyze which PRs need attention | |
| repo_attention=0 | |
| if [ "$count" -gt 0 ]; then | |
| prs=$(gh pr list --repo rundeck-plugins/$repo --state open --json number,title,updatedAt,author --limit 500) | |
| pr_count=$(echo "$prs" | jq 'length') | |
| for ((i=0; i<pr_count; i++)); do | |
| pr=$(echo "$prs" | jq -r ".[$i]") | |
| pr_number=$(echo "$pr" | jq -r '.number') | |
| pr_title=$(echo "$pr" | jq -r '.title') | |
| pr_updated=$(echo "$pr" | jq -r '.updatedAt') | |
| pr_author=$(echo "$pr" | jq -r '.author.login') | |
| # Get last comment and review, excluding Copilot | |
| last_comment=$(gh api "repos/rundeck-plugins/$repo/issues/$pr_number/comments" --paginate -q '.[] | select(.user.login != "copilot-pull-request-reviewer[bot]") | {user: .user.login, created_at: .created_at}' | jq -s '.[-1]') | |
| last_review=$(gh api "repos/rundeck-plugins/$repo/pulls/$pr_number/reviews" --paginate -q '.[] | select(.user.login != "copilot-pull-request-reviewer[bot]") | {user: .user.login, submitted_at: .submitted_at}' | jq -s '.[-1]') | |
| # Determine most recent activity | |
| comment_time=$(echo "$last_comment" | jq -r '.created_at // empty') | |
| review_time=$(echo "$last_review" | jq -r '.submitted_at // empty') | |
| if [ -z "$comment_time" ] && [ -z "$review_time" ]; then | |
| # No comments or reviews, use PR author | |
| last_actor="$pr_author" | |
| last_event="opened" | |
| elif [ -z "$review_time" ] || [[ "$comment_time" > "$review_time" ]]; then | |
| # Comment is more recent | |
| last_actor=$(echo "$last_comment" | jq -r '.user') | |
| last_event="commented" | |
| else | |
| # Review is more recent | |
| last_actor=$(echo "$last_review" | jq -r '.user') | |
| last_event="reviewed" | |
| fi | |
| # Check if last actor is community (not in team) | |
| if echo " $team_members " | grep -q " $last_actor "; then | |
| is_community=false | |
| else | |
| is_community=true | |
| fi | |
| # Debug output | |
| echo "DEBUG: $repo #$pr_number - last_actor='$last_actor' is_community='$is_community'" | |
| # Flag if community was last to act | |
| if [ "$is_community" = "true" ]; then | |
| repo_attention=$((repo_attention + 1)) | |
| # Save details for attention section | |
| echo "- **$repo #$pr_number**: $pr_title" >> attention-details.md | |
| echo " - Last activity: $last_event by @$last_actor" >> attention-details.md | |
| echo " - Link: https://github.com/rundeck-plugins/$repo/pull/$pr_number" >> attention-details.md | |
| echo "" >> attention-details.md | |
| fi | |
| done | |
| fi | |
| # Buffer the row (see rows.tsv comment above) instead of | |
| # writing straight to pr-count.md, so it can be sorted by | |
| # open-issue count before the table is written out. | |
| issues_link="[$issue_count](https://github.com/rundeck-plugins/$repo/issues)" | |
| if [ "$repo_attention" -gt 0 ]; then | |
| row="| [$repo](https://github.com/rundeck-plugins/$repo/pulls) | $count | $issues_link | **$repo_attention** |" | |
| else | |
| row="| [$repo](https://github.com/rundeck-plugins/$repo/pulls) | $count | $issues_link | 0 |" | |
| fi | |
| printf '%s\t%s\n' "$issue_count" "$row" >> rows.tsv | |
| total_prs=$((total_prs + count)) | |
| [ "$count" -gt 0 ] && repos_with_prs=$((repos_with_prs + 1)) | |
| total_issues=$((total_issues + issue_count)) | |
| [ "$issue_count" -gt 0 ] && repos_with_issues=$((repos_with_issues + 1)) | |
| needs_attention=$((needs_attention + repo_attention)) | |
| done | |
| echo "| Repository | Open PRs | Open Issues | Needs Attention |" >> pr-count.md | |
| echo "|------------|----------|-------------|-----------------|" >> pr-count.md | |
| sort -t $'\t' -k1,1nr rows.tsv | cut -f2- >> pr-count.md | |
| echo "" >> pr-count.md | |
| echo "**Total Open PRs:** $total_prs across $repos_with_prs repositories" >> pr-count.md | |
| echo "**Total Open Issues:** $total_issues across $repos_with_issues repositories" >> pr-count.md | |
| echo "**PRs Needing Attention:** $needs_attention" >> pr-count.md | |
| # Add attention details section | |
| if [ "$needs_attention" -gt 0 ]; then | |
| echo "" >> pr-count.md | |
| echo "---" >> pr-count.md | |
| echo "" >> pr-count.md | |
| echo "## PRs Needing Attention" >> pr-count.md | |
| echo "" >> pr-count.md | |
| echo "PRs where community has most recent activity (comment or commit):" >> pr-count.md | |
| echo "" >> pr-count.md | |
| cat attention-details.md >> pr-count.md | |
| fi | |
| # Create CSV with headers if it doesn't exist | |
| if [ ! -f pr-tracking/history.csv ]; then | |
| echo "date,total_prs,repos_with_prs,needs_attention" > pr-tracking/history.csv | |
| fi | |
| # Check if today's entry already exists, update or append | |
| today=$(date +%Y-%m-%d) | |
| # Share this exact value with later steps via $GITHUB_ENV instead | |
| # of letting them recompute their own `date` call - each `run:` | |
| # block is a fresh shell, so a later step's `date +%Y-%m-%d` could | |
| # in principle disagree with this one if the job happened to | |
| # straddle a UTC midnight between steps. | |
| echo "today=$today" >> "$GITHUB_ENV" | |
| if grep -q "^$today," pr-tracking/history.csv; then | |
| # Update existing entry - remove old line and append new | |
| grep -v "^$today," pr-tracking/history.csv > pr-tracking/history.csv.tmp | |
| mv pr-tracking/history.csv.tmp pr-tracking/history.csv | |
| echo "$today,$total_prs,$repos_with_prs,$needs_attention" >> pr-tracking/history.csv | |
| else | |
| # Append new entry | |
| echo "$today,$total_prs,$repos_with_prs,$needs_attention" >> pr-tracking/history.csv | |
| fi | |
| # Separate history file for issue counts (kept apart from | |
| # history.csv above so its existing schema/header don't shift | |
| # under already-written rows). | |
| if [ ! -f pr-tracking/issues-history.csv ]; then | |
| echo "date,total_issues,repos_with_issues" > pr-tracking/issues-history.csv | |
| fi | |
| if grep -q "^$today," pr-tracking/issues-history.csv; then | |
| grep -v "^$today," pr-tracking/issues-history.csv > pr-tracking/issues-history.csv.tmp | |
| mv pr-tracking/issues-history.csv.tmp pr-tracking/issues-history.csv | |
| fi | |
| echo "$today,$total_issues,$repos_with_issues" >> pr-tracking/issues-history.csv | |
| # Save markdown report (overwrite if exists) | |
| cp pr-count.md pr-tracking/$today.md | |
| cat pr-count.md | |
| - name: Check Release Drift | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Runs every day alongside the PR report (cheap: ~1-2 gh/API calls per | |
| # active repo, well under the Actions token's per-repo rate limit, and | |
| # lighter than the per-PR comment/review calls in the step above). | |
| # Appends a section into the same day's report file the daily job | |
| # already writes, so there's one report to read. | |
| # continue-on-error above means a bug here can't take down the daily | |
| # PR report that already ran and was written to disk. | |
| # $today comes from $GITHUB_ENV (set in the step above) rather | |
| # than a fresh `date` call, so this step's appends always land on | |
| # the exact same file that step wrote to. | |
| # Scoped to repos tagged with the "versioned-plugins" GitHub topic - | |
| # real plugins we cut releases for on a regular cadence, not tooling/ | |
| # example/demo repos that happen to carry semver tags for their own | |
| # reasons (build-zip, ui-job-metrics, etc.). "bundled-plugin" is a | |
| # narrower topic (subset actually consumed by rundeck/rundeckpro/ | |
| # ua-runner) used by the separate rundeck-plugin-versions skill. | |
| repos_json=$(gh repo list rundeck-plugins --topic versioned-plugins --no-archived --limit 100 --json name,defaultBranchRef) | |
| { | |
| echo "" | |
| echo "---" | |
| echo "" | |
| echo "## Release Drift" | |
| echo "" | |
| echo "Plugins tagged \`versioned-plugins\` with commits on their default branch beyond the latest GitHub Release - candidates for a release before the next Rundeck/Rundeckpro GA." | |
| echo "" | |
| echo "| Repository | Latest Release | Commits Ahead | Status |" | |
| echo "|------------|-----------------|----------------|--------|" | |
| } >> pr-tracking/$today.md | |
| needs_release=0 | |
| unknown=0 | |
| # defaultBranchRef comes from the same repo-list call above rather | |
| # than a per-repo `gh repo view`, to keep this cheap at scale. | |
| # Fed via process substitution (not a pipe) so the loop runs in the | |
| # current shell - needs_release/unknown must survive past the loop. | |
| while IFS=$'\t' read -r repo default_branch; do | |
| # GitHub Releases are the source of truth for "latest released version" | |
| # (same convention as skills/rundeck-plugin-versions) - raw git tags | |
| # include legacy v-prefixed / -grails7 / -SNAPSHOT noise. | |
| latest=$(gh release view --repo rundeck-plugins/$repo --json tagName -q .tagName 2>/dev/null || true) | |
| if [ -z "$latest" ]; then | |
| continue # never released (examples/demos/tooling repos) - not a drift candidate | |
| fi | |
| ahead=$(gh api "repos/rundeck-plugins/$repo/compare/$latest...$default_branch" --jq '.ahead_by' 2>/dev/null || true) | |
| if [ -z "$ahead" ]; then | |
| echo "| [$repo](https://github.com/rundeck-plugins/$repo) | $latest | ? | UNKNOWN |" >> pr-tracking/$today.md | |
| unknown=$((unknown + 1)) | |
| elif [ "$ahead" -gt 0 ]; then | |
| echo "| [$repo](https://github.com/rundeck-plugins/$repo/compare/$latest...$default_branch) | $latest | $ahead | **NEEDS RELEASE** |" >> pr-tracking/$today.md | |
| needs_release=$((needs_release + 1)) | |
| else | |
| echo "| $repo | $latest | 0 | OK |" >> pr-tracking/$today.md | |
| fi | |
| done < <(echo "$repos_json" | jq -r '.[] | "\(.name)\t\(.defaultBranchRef.name)"') | |
| { | |
| echo "" | |
| echo "**Plugins needing a release:** $needs_release ; **Unknown:** $unknown" | |
| } >> pr-tracking/$today.md | |
| # Separate daily history file (history.csv above stays PR-count-only) | |
| if [ ! -f pr-tracking/release-drift-history.csv ]; then | |
| echo "date,needs_release,unknown" > pr-tracking/release-drift-history.csv | |
| fi | |
| if grep -q "^$today," pr-tracking/release-drift-history.csv; then | |
| grep -v "^$today," pr-tracking/release-drift-history.csv > pr-tracking/release-drift-history.csv.tmp | |
| mv pr-tracking/release-drift-history.csv.tmp pr-tracking/release-drift-history.csv | |
| fi | |
| echo "$today,$needs_release,$unknown" >> pr-tracking/release-drift-history.csv | |
| - name: Commit and Push to pr-tracking branch | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Stable link to the latest report (same URL every day), taken | |
| # after the Release Drift step so it includes that section too - | |
| # copying earlier (e.g. right after pr-count.md is written) | |
| # would miss whatever that step appends to $today.md. History is | |
| # unaffected; pr-tracking/$today.md still exists for every day. | |
| # $today comes from $GITHUB_ENV (set in the first step) rather | |
| # than a fresh `date` call or a filesystem-mtime glob, so this is | |
| # guaranteed to be the exact file the earlier steps wrote to. | |
| cp "pr-tracking/$today.md" pr-tracking/latest.md | |
| git add pr-tracking/ | |
| git commit -m "Daily PR report: $today" || echo "No changes" | |
| git push origin pr-tracking |