Update release progress badges to use Priority-based categorization (#6270)

* Update release progress badges to use Priority-based categorization

- Replace "Release Blockers"/"Release Must Do" with "Critical Priority"/"High Priority"
- Switch issue filtering from Release gating field to Priority field (P0 - Critical/P1 - High)
- Remove "Nice to Have" badge from workflow and README
- Add failure step: on workflow error, update both badge gist files to bright
  purple (BF00FF) with "FAILURE" message
- Version auto-detected from src/H5public.h for milestone filtering

* Fix stale class docstring: release blocker -> priority issue
This commit is contained in:
Scot Breitenfeld
2026-03-04 16:11:52 -06:00
committed by GitHub
parent 889e48b655
commit 6e7fec1cc2
4 changed files with 97 additions and 120 deletions
+21 -59
View File
@@ -1,7 +1,7 @@
#!/bin/bash
#
# Badge Generation and Gist Update Script
# Generates badges for release blocker and must-do progress and updates GitHub Gist
# Generates badges for critical and high priority issue progress and updates GitHub Gist
#
# Environment Variables Required:
# GITHUB_TOKEN - GitHub token for Gist API access
@@ -11,12 +11,10 @@
# PERCENTAGE - Overall completion percentage
# DONE - Number of completed items
# TOTAL - Total number of items
# BLOCKER_DONE - Number of completed blockers
# BLOCKER_TOTAL - Total number of blockers
# MUSTDO_DONE - Number of completed must-dos
# MUSTDO_TOTAL - Total number of must-dos
# NICETOHAVE_DONE - Number of completed nice-to-haves
# NICETOHAVE_TOTAL - Total number of nice-to-haves
# BLOCKER_DONE - Number of completed critical priority items
# BLOCKER_TOTAL - Total number of critical priority items
# MUSTDO_DONE - Number of completed high priority items
# MUSTDO_TOTAL - Total number of high priority items
# VERSION - Version string (e.g., "2.1") - optional
#
@@ -120,12 +118,6 @@ else
MUSTDO_PERCENTAGE=$(awk "BEGIN {printf \"%.1f\", ($MUSTDO_DONE / $MUSTDO_TOTAL * 100)}")
fi
if [ "$NICETOHAVE_TOTAL" -eq 0 ]; then
NICETOHAVE_PERCENTAGE="-1.0"
else
NICETOHAVE_PERCENTAGE=$(awk "BEGIN {printf \"%.1f\", ($NICETOHAVE_DONE / $NICETOHAVE_TOTAL * 100)}")
fi
# Determine colors using the shared function (use lightgrey for 0/0)
if [ "$BLOCKER_PERCENTAGE" = "-1.0" ]; then
BLOCKER_COLOR="lightgrey"
@@ -139,56 +131,40 @@ else
MUSTDO_COLOR=$(get_badge_color "$MUSTDO_PERCENTAGE")
fi
if [ "$NICETOHAVE_PERCENTAGE" = "-1.0" ]; then
NICETOHAVE_COLOR="lightgrey"
else
NICETOHAVE_COLOR=$(get_badge_color "$NICETOHAVE_PERCENTAGE")
fi
# Determine badge labels - include version if available
if [ -n "${VERSION:-}" ] && [ "$VERSION" != "all" ]; then
BLOCKER_LABEL="${VERSION} Release Blockers"
MUSTDO_LABEL="${VERSION} Release Must Do"
NICETOHAVE_LABEL="${VERSION} Release Nice to Have"
BLOCKER_LABEL="${VERSION} Critical Priority"
MUSTDO_LABEL="${VERSION} High Priority"
else
BLOCKER_LABEL="Release Blockers"
MUSTDO_LABEL="Release Must Do"
NICETOHAVE_LABEL="Release Nice to Have"
BLOCKER_LABEL="Critical Priority"
MUSTDO_LABEL="High Priority"
fi
# Create badge JSONs using the shared function
BLOCKER_BADGE_JSON=$(create_badge_json "$BLOCKER_LABEL" "$BLOCKER_DONE" "$BLOCKER_TOTAL" "$BLOCKER_PERCENTAGE" "$BLOCKER_COLOR")
MUSTDO_BADGE_JSON=$(create_badge_json "$MUSTDO_LABEL" "$MUSTDO_DONE" "$MUSTDO_TOTAL" "$MUSTDO_PERCENTAGE" "$MUSTDO_COLOR")
NICETOHAVE_BADGE_JSON=$(create_badge_json "$NICETOHAVE_LABEL" "$NICETOHAVE_DONE" "$NICETOHAVE_TOTAL" "$NICETOHAVE_PERCENTAGE" "$NICETOHAVE_COLOR")
# Validate JSONs were created successfully
if [ -z "$BLOCKER_BADGE_JSON" ] || ! echo "$BLOCKER_BADGE_JSON" | jq empty 2>/dev/null; then
echo "::error::Failed to generate valid blocker badge JSON"
echo "::error::Failed to generate valid critical priority badge JSON"
exit 1
fi
if [ -z "$MUSTDO_BADGE_JSON" ] || ! echo "$MUSTDO_BADGE_JSON" | jq empty 2>/dev/null; then
echo "::error::Failed to generate valid must-do badge JSON"
exit 1
fi
if [ -z "$NICETOHAVE_BADGE_JSON" ] || ! echo "$NICETOHAVE_BADGE_JSON" | jq empty 2>/dev/null; then
echo "::error::Failed to generate valid nice-to-have badge JSON"
echo "::error::Failed to generate valid high priority badge JSON"
exit 1
fi
# The filenames in the Gist
BLOCKER_GIST_NAME="release-blocker-${GITHUB_REPOSITORY##*/}.json"
MUSTDO_GIST_NAME="release-mustdo-${GITHUB_REPOSITORY##*/}.json"
NICETOHAVE_GIST_NAME="release-nicetohave-${GITHUB_REPOSITORY##*/}.json"
echo "::notice::Updating Gist files: $BLOCKER_GIST_NAME, $MUSTDO_GIST_NAME, $NICETOHAVE_GIST_NAME"
echo "::notice::Updating Gist files: $BLOCKER_GIST_NAME, $MUSTDO_GIST_NAME"
# Create the request payload with all three files
# Create the request payload with both files
REQUEST_PAYLOAD=$(jq -n \
--arg blocker_filename "$BLOCKER_GIST_NAME" \
--arg mustdo_filename "$MUSTDO_GIST_NAME" \
--arg nicetohave_filename "$NICETOHAVE_GIST_NAME" \
--argjson blocker_content "$BLOCKER_BADGE_JSON" \
--argjson mustdo_content "$MUSTDO_BADGE_JSON" \
--argjson nicetohave_content "$NICETOHAVE_BADGE_JSON" \
'{
"files": {
($blocker_filename): {
@@ -196,9 +172,6 @@ REQUEST_PAYLOAD=$(jq -n \
},
($mustdo_filename): {
"content": ($mustdo_content | tostring)
},
($nicetohave_filename): {
"content": ($nicetohave_content | tostring)
}
}
}')
@@ -227,18 +200,15 @@ echo "::notice::Gist updated successfully"
# Generate badge URLs for use in README
BLOCKER_BADGE_URL="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/${GITHUB_REPOSITORY_OWNER}-Bot/${GIST_ID}/raw/${BLOCKER_GIST_NAME}"
MUSTDO_BADGE_URL="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/${GITHUB_REPOSITORY_OWNER}-Bot/${GIST_ID}/raw/${MUSTDO_GIST_NAME}"
NICETOHAVE_BADGE_URL="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/${GITHUB_REPOSITORY_OWNER}-Bot/${GIST_ID}/raw/${NICETOHAVE_GIST_NAME}"
PROJECT_URL="https://github.com/${GITHUB_REPOSITORY}/projects/39"
echo "::notice::Blocker Badge URL: $BLOCKER_BADGE_URL"
echo "::notice::Must-Do Badge URL: $MUSTDO_BADGE_URL"
echo "::notice::Nice-to-Have Badge URL: $NICETOHAVE_BADGE_URL"
echo "::notice::Critical Priority Badge URL: $BLOCKER_BADGE_URL"
echo "::notice::High Priority Badge URL: $MUSTDO_BADGE_URL"
# Output to GitHub Actions if GITHUB_OUTPUT is set
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "blocker_badge_url=$BLOCKER_BADGE_URL" >> "$GITHUB_OUTPUT"
echo "mustdo_badge_url=$MUSTDO_BADGE_URL" >> "$GITHUB_OUTPUT"
echo "nicetohave_badge_url=$NICETOHAVE_BADGE_URL" >> "$GITHUB_OUTPUT"
echo "project_url=$PROJECT_URL" >> "$GITHUB_OUTPUT"
fi
@@ -250,35 +220,27 @@ if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
**Status:** ${STATUS}
**Overall Progress:** ${DONE} of ${TOTAL} items completed
### 🚫 Release Blockers
### Critical Priority
**Progress:** ${BLOCKER_DONE} of ${BLOCKER_TOTAL} completed (${BLOCKER_PERCENTAGE}%)
**Badge Color:** ${BLOCKER_COLOR}
### ✅ Release Must Do
### High Priority
**Progress:** ${MUSTDO_DONE} of ${MUSTDO_TOTAL} completed (${MUSTDO_PERCENTAGE}%)
**Badge Color:** ${MUSTDO_COLOR}
### 💡 Release Nice to Have
**Progress:** ${NICETOHAVE_DONE} of ${NICETOHAVE_TOTAL} completed (${NICETOHAVE_PERCENTAGE}%)
**Badge Color:** ${NICETOHAVE_COLOR}
**Gist ID:** ${GIST_ID}
### Badge URLs
**Blocker Markdown:** \`[![Release Blocker Progress](${BLOCKER_BADGE_URL})](${PROJECT_URL})\`
**Must-Do Markdown:** \`[![Release Must Do Progress](${MUSTDO_BADGE_URL})](${PROJECT_URL})\`
**Nice-to-Have Markdown:** \`[![Release Nice to Have Progress](${NICETOHAVE_BADGE_URL})](${PROJECT_URL})\`
**Critical Priority Markdown:** \`[![Critical Priority Progress](${BLOCKER_BADGE_URL})](${PROJECT_URL})\`
**High Priority Markdown:** \`[![High Priority Progress](${MUSTDO_BADGE_URL})](${PROJECT_URL})\`
### Badge JSON Preview
\`\`\`json
// Blocker Badge
// Critical Priority Badge
${BLOCKER_BADGE_JSON}
// Must-Do Badge
// High Priority Badge
${MUSTDO_BADGE_JSON}
// Nice-to-Have Badge
${NICETOHAVE_BADGE_JSON}
\`\`\`
EOF
fi
+30 -44
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
GitHub Project Release Blocker Progress Tracker
Fetches release blocker issues from the HDF5 project and calculates completion percentage.
GitHub Project Priority Issue Progress Tracker
Fetches critical and high priority issues from the HDF5 project and calculates completion percentage.
"""
import os
@@ -22,13 +22,12 @@ class ProjectDataError(Exception):
# Configuration: Expected field names in GitHub Project
# Update these if the project field names change
FIELD_RELEASE_GATING = "Release gating"
FIELD_PRIORITY = "Priority"
FIELD_STATUS = "Status"
# Expected values for Release gating field
VALUE_RELEASE_BLOCKER = "Release_Blocker"
VALUE_RELEASE_MUST_DO = "Release_Must Do"
VALUE_RELEASE_NICE_TO_HAVE = "Release_Nice to Have"
# Expected values for Priority field
VALUE_CRITICAL = "P0 - Critical"
VALUE_HIGH = "P1 - High"
# Expected value for Status field when an item is completed
VALUE_STATUS_DONE = "Done"
@@ -39,7 +38,7 @@ DEFAULT_MILESTONE_FILTER = None # Will be set from environment or H5public.h
class GitHubProjectTracker:
"""Tracks release blocker progress in GitHub projects."""
"""Tracks priority issue progress in GitHub projects."""
def __init__(self, token: str, owner: str, project_number: int, milestone_filter: Optional[str] = None):
self.api_url = "https://api.github.com/graphql"
@@ -126,22 +125,20 @@ class GitHubProjectTracker:
def fetch_release_blocker_stats(self) -> Dict[str, int]:
"""
Fetches release blocker, must-do, and nice-to-have statistics from the GitHub project.
Fetches critical and high priority issue statistics from the GitHub project.
Returns:
Dict with 'total', 'done', 'percentage', 'blocker_total', 'blocker_done',
'mustdo_total', 'mustdo_done', 'nicetohave_total', 'nicetohave_done' keys
'mustdo_total', 'mustdo_done' keys
"""
blocker_total = 0
blocker_done = 0
mustdo_total = 0
mustdo_done = 0
nicetohave_total = 0
nicetohave_done = 0
cursor = None
# Track if we've seen the expected fields at least once
seen_release_gating = False
seen_priority = False
seen_status = False
while True:
@@ -189,26 +186,22 @@ class GitHubProjectTracker:
fields = self._parse_item_fields(item)
# Validate expected fields exist
if FIELD_RELEASE_GATING in fields:
seen_release_gating = True
if FIELD_PRIORITY in fields:
seen_priority = True
if FIELD_STATUS in fields:
seen_status = True
release_gating = fields.get(FIELD_RELEASE_GATING, "")
priority = fields.get(FIELD_PRIORITY, "")
status = fields.get(FIELD_STATUS, "")
if release_gating == VALUE_RELEASE_BLOCKER:
if priority == VALUE_CRITICAL:
blocker_total += 1
if status == VALUE_STATUS_DONE:
blocker_done += 1
elif release_gating == VALUE_RELEASE_MUST_DO:
elif priority == VALUE_HIGH:
mustdo_total += 1
if status == VALUE_STATUS_DONE:
mustdo_done += 1
elif release_gating == VALUE_RELEASE_NICE_TO_HAVE:
nicetohave_total += 1
if status == VALUE_STATUS_DONE:
nicetohave_done += 1
# Check for next page
page_info = items.get("pageInfo", {})
@@ -217,18 +210,18 @@ class GitHubProjectTracker:
cursor = page_info.get("endCursor")
# Validate that expected fields were found - FAIL HARD if missing
# This prevents false positives where field renames would cause 0 blockers to be reported
if not seen_release_gating:
print(f"ERROR: Critical field '{FIELD_RELEASE_GATING}' not found in any project items.",
# This prevents false positives where field renames would cause 0 items to be reported
if not seen_priority:
print(f"ERROR: Critical field '{FIELD_PRIORITY}' not found in any project items.",
file=sys.stderr)
print("This field is required to identify release blockers and must-do items.",
print("This field is required to identify critical and high priority items.",
file=sys.stderr)
print("Possible causes:", file=sys.stderr)
print(f" 1. Field '{FIELD_RELEASE_GATING}' was renamed in the project", file=sys.stderr)
print(f" 1. Field '{FIELD_PRIORITY}' was renamed in the project", file=sys.stderr)
print(" 2. Project structure changed", file=sys.stderr)
print(" 3. Project is empty or inaccessible", file=sys.stderr)
print("Action required: Update FIELD_RELEASE_GATING constant in this script.", file=sys.stderr)
raise ProjectFieldMissingError(f"Critical field '{FIELD_RELEASE_GATING}' not found")
print("Action required: Update FIELD_PRIORITY constant in this script.", file=sys.stderr)
raise ProjectFieldMissingError(f"Critical field '{FIELD_PRIORITY}' not found")
if not seen_status:
print(f"ERROR: Critical field '{FIELD_STATUS}' not found in any project items.",
@@ -248,19 +241,19 @@ class GitHubProjectTracker:
# If total is 0, either the project is empty or field matching failed
if total == 0:
if self.milestone_filter:
print(f"INFO: No release blocker or must-do items found for milestone '{self.milestone_filter}'.", file=sys.stderr)
print(f"INFO: No critical or high priority items found for milestone '{self.milestone_filter}'.", file=sys.stderr)
print("This may be expected if no items exist for this milestone yet.", file=sys.stderr)
# Don't fail - return N/A indicators when filtering by milestone with no items
percentage = -1.0 # Use -1 to indicate N/A
else:
print("ERROR: No release blocker or must-do items found (total=0).", file=sys.stderr)
print("ERROR: No critical or high priority items found (total=0).", file=sys.stderr)
print("This likely indicates:", file=sys.stderr)
print(f" 1. The '{FIELD_RELEASE_GATING}' field values changed", file=sys.stderr)
print(f" Expected values: '{VALUE_RELEASE_BLOCKER}' or '{VALUE_RELEASE_MUST_DO}'", file=sys.stderr)
print(f" 1. The '{FIELD_PRIORITY}' field values changed", file=sys.stderr)
print(f" Expected values: '{VALUE_CRITICAL}' or '{VALUE_HIGH}'", file=sys.stderr)
print(" 2. Project has no items with these field values", file=sys.stderr)
print(" 3. Field matching logic needs to be updated", file=sys.stderr)
print("Refusing to report 0% or 100% with no items to prevent false positives.", file=sys.stderr)
raise ProjectDataError("No release items found - refusing to report false completion status")
raise ProjectDataError("No priority items found - refusing to report false completion status")
else:
percentage = round((done / total * 100), 1)
@@ -271,9 +264,7 @@ class GitHubProjectTracker:
'blocker_total': blocker_total,
'blocker_done': blocker_done,
'mustdo_total': mustdo_total,
'mustdo_done': mustdo_done,
'nicetohave_total': nicetohave_total,
'nicetohave_done': nicetohave_done
'mustdo_done': mustdo_done
}
@@ -346,8 +337,6 @@ def main():
f.write(f"blocker_done={stats['blocker_done']}\n")
f.write(f"mustdo_total={stats['mustdo_total']}\n")
f.write(f"mustdo_done={stats['mustdo_done']}\n")
f.write(f"nicetohave_total={stats['nicetohave_total']}\n")
f.write(f"nicetohave_done={stats['nicetohave_done']}\n")
f.write(f"version={MILESTONE_FILTER or 'all'}\n")
# Also output to stdout for local testing
@@ -356,14 +345,11 @@ def main():
print(f"blocker_total={stats['blocker_total']}")
print(f"mustdo_done={stats['mustdo_done']}")
print(f"mustdo_total={stats['mustdo_total']}")
print(f"nicetohave_done={stats['nicetohave_done']}")
print(f"nicetohave_total={stats['nicetohave_total']}")
print(f"version={MILESTONE_FILTER or 'all'}")
print(f"Calculated progress: {stats['percentage']}%")
print(f"Done / Total: {stats['done']} / {stats['total']}")
print(f"Blockers: {stats['blocker_done']} / {stats['blocker_total']}")
print(f"Must Do: {stats['mustdo_done']} / {stats['mustdo_total']}")
print(f"Nice to Have: {stats['nicetohave_done']} / {stats['nicetohave_total']}")
print(f"Critical Priority: {stats['blocker_done']} / {stats['blocker_total']}")
print(f"High Priority: {stats['mustdo_done']} / {stats['mustdo_total']}")
if MILESTONE_FILTER:
print(f"Milestone filter: {MILESTONE_FILTER}")
+43 -12
View File
@@ -115,13 +115,11 @@ jobs:
exit 1
fi
# Extract blocker, must-do, and nice-to-have counts
# Extract critical and high priority counts
BLOCKER_DONE=$(grep "^blocker_done=" progress_output.txt | cut -d'=' -f2 | head -1)
BLOCKER_TOTAL=$(grep "^blocker_total=" progress_output.txt | cut -d'=' -f2 | head -1)
MUSTDO_DONE=$(grep "^mustdo_done=" progress_output.txt | cut -d'=' -f2 | head -1)
MUSTDO_TOTAL=$(grep "^mustdo_total=" progress_output.txt | cut -d'=' -f2 | head -1)
NICETOHAVE_DONE=$(grep "^nicetohave_done=" progress_output.txt | cut -d'=' -f2 | head -1)
NICETOHAVE_TOTAL=$(grep "^nicetohave_total=" progress_output.txt | cut -d'=' -f2 | head -1)
VERSION=$(grep "^version=" progress_output.txt | cut -d'=' -f2 | head -1)
# Set outputs for use in subsequent steps
@@ -132,12 +130,10 @@ jobs:
echo "blocker_total=$BLOCKER_TOTAL" >> $GITHUB_OUTPUT
echo "mustdo_done=$MUSTDO_DONE" >> $GITHUB_OUTPUT
echo "mustdo_total=$MUSTDO_TOTAL" >> $GITHUB_OUTPUT
echo "nicetohave_done=$NICETOHAVE_DONE" >> $GITHUB_OUTPUT
echo "nicetohave_total=$NICETOHAVE_TOTAL" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "::notice::Progress calculation successful: ${PERCENTAGE}% (${DONE}/${TOTAL})"
echo "::notice:: Version: ${VERSION}, Blockers: ${BLOCKER_DONE}/${BLOCKER_TOTAL}, Must Do: ${MUSTDO_DONE}/${MUSTDO_TOTAL}, Nice to Have: ${NICETOHAVE_DONE}/${NICETOHAVE_TOTAL}"
echo "::notice:: Version: ${VERSION}, Critical Priority: ${BLOCKER_DONE}/${BLOCKER_TOTAL}, High Priority: ${MUSTDO_DONE}/${MUSTDO_TOTAL}"
# Clean up
rm -f progress_output.txt
@@ -158,17 +154,52 @@ jobs:
BLOCKER_TOTAL: ${{ steps.progress.outputs.blocker_total }}
MUSTDO_DONE: ${{ steps.progress.outputs.mustdo_done }}
MUSTDO_TOTAL: ${{ steps.progress.outputs.mustdo_total }}
NICETOHAVE_DONE: ${{ steps.progress.outputs.nicetohave_done }}
NICETOHAVE_TOTAL: ${{ steps.progress.outputs.nicetohave_total }}
VERSION: ${{ steps.progress.outputs.version }}
run: |
# Execute dedicated badge generation script
# This separates concerns: YAML orchestrates, scripts implement logic
bash .github/workflows/update-badge.sh
- name: Cleanup on failure
- name: Update failure badge on error
if: failure()
env:
GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }}
GIST_ID: ${{ secrets.GIST_ID }}
run: |
echo "::warning::Workflow failed - cleaning up temporary files"
rm -f progress_output.txt
echo "::notice::Check the logs above for specific error details"
echo "::warning::Workflow failed - updating badges to FAILURE state"
REPO_NAME="${GITHUB_REPOSITORY##*/}"
BLOCKER_GIST_NAME="release-blocker-${REPO_NAME}.json"
MUSTDO_GIST_NAME="release-mustdo-${REPO_NAME}.json"
FAILURE_BADGE=$(jq -n '{
"schemaVersion": 1,
"label": "Release Progress",
"message": "FAILURE",
"color": "BF00FF",
"style": "flat-square"
}')
REQUEST_PAYLOAD=$(jq -n \
--arg blocker_filename "$BLOCKER_GIST_NAME" \
--arg mustdo_filename "$MUSTDO_GIST_NAME" \
--argjson content "$FAILURE_BADGE" \
'{
"files": {
($blocker_filename): { "content": ($content | tostring) },
($mustdo_filename): { "content": ($content | tostring) }
}
}')
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -L -X PATCH \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/gists/${GIST_ID}" \
-d "$REQUEST_PAYLOAD")
if [ "$HTTP_CODE" != "200" ]; then
echo "::warning::Failed to update failure badges (HTTP $HTTP_CODE)"
else
echo "::notice::Failure badges updated successfully"
fi