Skip to content

PR Tracking and Attention Report #225

PR Tracking and Attention Report

PR Tracking and Attention Report #225

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