mirror of
https://github.com/HDFGroup/hdf5.git
synced 2026-09-25 04:09:44 +03:00
Add release-progress badges: Next/Latest Release, Medium/Low priority (#6537)
* Add Next Release badge and Medium/Low priority progress badges Adds a "Next Release" badge (in-development version from H5public.h) and extends the existing Critical/High priority progress badges with Medium (P2) and Low (P3) priority tracking, all fed from the same GitHub Project #39 gist-based badge pipeline. * Remove in-progress 2.0 entry from release schedule diagram The Release Schedule chart is meant to show only past releases that have reached end of life; the in-development series is now tracked separately by the Release Progress badges. Regenerate the PNG from the updated PlantUML source and clarify the README wording. * Add Latest Release badge and milestone target date to Next Release Fetches the most recent published release in the current major version series from the GitHub Releases API (e.g. "2.1.1 (2026-03-23)") and exposes it as a new "Latest Release" badge. Also looks up the target due date of the matching GitHub milestone (e.g. "HDF5 2.2.0") and appends it to the existing Next Release badge when one is set. Both lookups are independent of the project-board query and degrade gracefully to "N/A" on failure.
This commit is contained in:
@@ -15,7 +15,14 @@
|
||||
# BLOCKER_TOTAL - Total number of critical priority items
|
||||
# MUSTDO_DONE - Number of completed high priority items
|
||||
# MUSTDO_TOTAL - Total number of high priority items
|
||||
# MEDIUM_DONE - Number of completed medium priority items
|
||||
# MEDIUM_TOTAL - Total number of medium priority items
|
||||
# LOW_DONE - Number of completed low priority items
|
||||
# LOW_TOTAL - Total number of low priority items
|
||||
# VERSION - Version string (e.g., "2.1") - optional
|
||||
# LATEST_RELEASE_TAG - Most recent published release tag in the current major series - optional
|
||||
# LATEST_RELEASE_DATE - Publish date (YYYY-MM-DD) of LATEST_RELEASE_TAG - optional
|
||||
# MILESTONE_DUE_DATE - Target due date (YYYY-MM-DD) of the in-development milestone - optional
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
@@ -33,6 +40,10 @@ required_vars=(
|
||||
"BLOCKER_TOTAL"
|
||||
"MUSTDO_DONE"
|
||||
"MUSTDO_TOTAL"
|
||||
"MEDIUM_DONE"
|
||||
"MEDIUM_TOTAL"
|
||||
"LOW_DONE"
|
||||
"LOW_TOTAL"
|
||||
)
|
||||
|
||||
for var in "${required_vars[@]}"; do
|
||||
@@ -131,18 +142,48 @@ else
|
||||
MUSTDO_COLOR=$(get_badge_color "$MUSTDO_PERCENTAGE")
|
||||
fi
|
||||
|
||||
if [ "$MEDIUM_TOTAL" -eq 0 ]; then
|
||||
MEDIUM_PERCENTAGE="-1.0"
|
||||
else
|
||||
MEDIUM_PERCENTAGE=$(awk "BEGIN {printf \"%.1f\", ($MEDIUM_DONE / $MEDIUM_TOTAL * 100)}")
|
||||
fi
|
||||
|
||||
if [ "$LOW_TOTAL" -eq 0 ]; then
|
||||
LOW_PERCENTAGE="-1.0"
|
||||
else
|
||||
LOW_PERCENTAGE=$(awk "BEGIN {printf \"%.1f\", ($LOW_DONE / $LOW_TOTAL * 100)}")
|
||||
fi
|
||||
|
||||
if [ "$MEDIUM_PERCENTAGE" = "-1.0" ]; then
|
||||
MEDIUM_COLOR="lightgrey"
|
||||
else
|
||||
MEDIUM_COLOR=$(get_badge_color "$MEDIUM_PERCENTAGE")
|
||||
fi
|
||||
|
||||
if [ "$LOW_PERCENTAGE" = "-1.0" ]; then
|
||||
LOW_COLOR="lightgrey"
|
||||
else
|
||||
LOW_COLOR=$(get_badge_color "$LOW_PERCENTAGE")
|
||||
fi
|
||||
|
||||
# Determine badge labels - include version if available
|
||||
if [ -n "${VERSION:-}" ] && [ "$VERSION" != "all" ]; then
|
||||
BLOCKER_LABEL="${VERSION} Critical Priority"
|
||||
MUSTDO_LABEL="${VERSION} High Priority"
|
||||
MEDIUM_LABEL="${VERSION} Medium Priority"
|
||||
LOW_LABEL="${VERSION} Low Priority"
|
||||
else
|
||||
BLOCKER_LABEL="Critical Priority"
|
||||
MUSTDO_LABEL="High Priority"
|
||||
MEDIUM_LABEL="Medium Priority"
|
||||
LOW_LABEL="Low 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")
|
||||
MEDIUM_BADGE_JSON=$(create_badge_json "$MEDIUM_LABEL" "$MEDIUM_DONE" "$MEDIUM_TOTAL" "$MEDIUM_PERCENTAGE" "$MEDIUM_COLOR")
|
||||
LOW_BADGE_JSON=$(create_badge_json "$LOW_LABEL" "$LOW_DONE" "$LOW_TOTAL" "$LOW_PERCENTAGE" "$LOW_COLOR")
|
||||
|
||||
# Validate JSONs were created successfully
|
||||
if [ -z "$BLOCKER_BADGE_JSON" ] || ! echo "$BLOCKER_BADGE_JSON" | jq empty 2>/dev/null; then
|
||||
@@ -153,18 +194,98 @@ if [ -z "$MUSTDO_BADGE_JSON" ] || ! echo "$MUSTDO_BADGE_JSON" | jq empty 2>/dev/
|
||||
echo "::error::Failed to generate valid high priority badge JSON"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$MEDIUM_BADGE_JSON" ] || ! echo "$MEDIUM_BADGE_JSON" | jq empty 2>/dev/null; then
|
||||
echo "::error::Failed to generate valid medium priority badge JSON"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$LOW_BADGE_JSON" ] || ! echo "$LOW_BADGE_JSON" | jq empty 2>/dev/null; then
|
||||
echo "::error::Failed to generate valid low priority badge JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# "Next Release" badge - reports the in-development version (derived from
|
||||
# H5_VERS_MAJOR/H5_VERS_MINOR in src/H5public.h by update-progress.py), the
|
||||
# same value used as the label prefix above, annotated with the matching
|
||||
# milestone's target due date when one is set. Purely informational, so it
|
||||
# isn't colored by completion percentage.
|
||||
if [ -n "${VERSION:-}" ] && [ "$VERSION" != "all" ]; then
|
||||
if [ -n "${MILESTONE_DUE_DATE:-}" ]; then
|
||||
VERSION_MESSAGE="$VERSION (target: $MILESTONE_DUE_DATE)"
|
||||
else
|
||||
VERSION_MESSAGE="$VERSION"
|
||||
fi
|
||||
VERSION_COLOR="blue"
|
||||
else
|
||||
VERSION_MESSAGE="N/A"
|
||||
VERSION_COLOR="lightgrey"
|
||||
fi
|
||||
|
||||
VERSION_BADGE_JSON=$(jq -n \
|
||||
--arg message "$VERSION_MESSAGE" \
|
||||
--arg color "$VERSION_COLOR" \
|
||||
'{
|
||||
"schemaVersion": 1,
|
||||
"label": "Next Release",
|
||||
"message": $message,
|
||||
"color": $color,
|
||||
"style": "flat-square"
|
||||
}')
|
||||
|
||||
if [ -z "$VERSION_BADGE_JSON" ] || ! echo "$VERSION_BADGE_JSON" | jq empty 2>/dev/null; then
|
||||
echo "::error::Failed to generate valid next release badge JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# "Latest Release" badge - reports the most recent published release in the
|
||||
# current major version series (e.g. "2.1.1 (2026-03-23)"), fetched directly
|
||||
# from the GitHub Releases API. Purely informational.
|
||||
if [ -n "${LATEST_RELEASE_TAG:-}" ]; then
|
||||
LATEST_RELEASE_MESSAGE="${LATEST_RELEASE_TAG} (${LATEST_RELEASE_DATE:-unknown date})"
|
||||
LATEST_RELEASE_COLOR="blue"
|
||||
else
|
||||
LATEST_RELEASE_MESSAGE="N/A"
|
||||
LATEST_RELEASE_COLOR="lightgrey"
|
||||
fi
|
||||
|
||||
LATEST_RELEASE_BADGE_JSON=$(jq -n \
|
||||
--arg message "$LATEST_RELEASE_MESSAGE" \
|
||||
--arg color "$LATEST_RELEASE_COLOR" \
|
||||
'{
|
||||
"schemaVersion": 1,
|
||||
"label": "Latest Release",
|
||||
"message": $message,
|
||||
"color": $color,
|
||||
"style": "flat-square"
|
||||
}')
|
||||
|
||||
if [ -z "$LATEST_RELEASE_BADGE_JSON" ] || ! echo "$LATEST_RELEASE_BADGE_JSON" | jq empty 2>/dev/null; then
|
||||
echo "::error::Failed to generate valid latest release 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"
|
||||
echo "::notice::Updating Gist files: $BLOCKER_GIST_NAME, $MUSTDO_GIST_NAME"
|
||||
MEDIUM_GIST_NAME="release-medium-${GITHUB_REPOSITORY##*/}.json"
|
||||
LOW_GIST_NAME="release-low-${GITHUB_REPOSITORY##*/}.json"
|
||||
VERSION_GIST_NAME="release-version-${GITHUB_REPOSITORY##*/}.json"
|
||||
LATEST_RELEASE_GIST_NAME="release-latest-${GITHUB_REPOSITORY##*/}.json"
|
||||
echo "::notice::Updating Gist files: $BLOCKER_GIST_NAME, $MUSTDO_GIST_NAME, $MEDIUM_GIST_NAME, $LOW_GIST_NAME, $VERSION_GIST_NAME, $LATEST_RELEASE_GIST_NAME"
|
||||
|
||||
# Create the request payload with both files
|
||||
# Create the request payload with all six files
|
||||
REQUEST_PAYLOAD=$(jq -n \
|
||||
--arg blocker_filename "$BLOCKER_GIST_NAME" \
|
||||
--arg mustdo_filename "$MUSTDO_GIST_NAME" \
|
||||
--arg medium_filename "$MEDIUM_GIST_NAME" \
|
||||
--arg low_filename "$LOW_GIST_NAME" \
|
||||
--arg version_filename "$VERSION_GIST_NAME" \
|
||||
--arg latest_release_filename "$LATEST_RELEASE_GIST_NAME" \
|
||||
--argjson blocker_content "$BLOCKER_BADGE_JSON" \
|
||||
--argjson mustdo_content "$MUSTDO_BADGE_JSON" \
|
||||
--argjson medium_content "$MEDIUM_BADGE_JSON" \
|
||||
--argjson low_content "$LOW_BADGE_JSON" \
|
||||
--argjson version_content "$VERSION_BADGE_JSON" \
|
||||
--argjson latest_release_content "$LATEST_RELEASE_BADGE_JSON" \
|
||||
'{
|
||||
"files": {
|
||||
($blocker_filename): {
|
||||
@@ -172,6 +293,18 @@ REQUEST_PAYLOAD=$(jq -n \
|
||||
},
|
||||
($mustdo_filename): {
|
||||
"content": ($mustdo_content | tostring)
|
||||
},
|
||||
($medium_filename): {
|
||||
"content": ($medium_content | tostring)
|
||||
},
|
||||
($low_filename): {
|
||||
"content": ($low_content | tostring)
|
||||
},
|
||||
($version_filename): {
|
||||
"content": ($version_content | tostring)
|
||||
},
|
||||
($latest_release_filename): {
|
||||
"content": ($latest_release_content | tostring)
|
||||
}
|
||||
}
|
||||
}')
|
||||
@@ -200,15 +333,28 @@ 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}"
|
||||
MEDIUM_BADGE_URL="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/${GITHUB_REPOSITORY_OWNER}-Bot/${GIST_ID}/raw/${MEDIUM_GIST_NAME}"
|
||||
LOW_BADGE_URL="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/${GITHUB_REPOSITORY_OWNER}-Bot/${GIST_ID}/raw/${LOW_GIST_NAME}"
|
||||
VERSION_BADGE_URL="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/${GITHUB_REPOSITORY_OWNER}-Bot/${GIST_ID}/raw/${VERSION_GIST_NAME}"
|
||||
LATEST_RELEASE_BADGE_URL="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/${GITHUB_REPOSITORY_OWNER}-Bot/${GIST_ID}/raw/${LATEST_RELEASE_GIST_NAME}"
|
||||
PROJECT_URL="https://github.com/${GITHUB_REPOSITORY}/projects/39"
|
||||
RELEASES_URL="https://github.com/${GITHUB_REPOSITORY}/releases"
|
||||
|
||||
echo "::notice::Critical Priority Badge URL: $BLOCKER_BADGE_URL"
|
||||
echo "::notice::High Priority Badge URL: $MUSTDO_BADGE_URL"
|
||||
echo "::notice::Medium Priority Badge URL: $MEDIUM_BADGE_URL"
|
||||
echo "::notice::Low Priority Badge URL: $LOW_BADGE_URL"
|
||||
echo "::notice::Next Release Badge URL: $VERSION_BADGE_URL"
|
||||
echo "::notice::Latest Release Badge URL: $LATEST_RELEASE_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 "medium_badge_url=$MEDIUM_BADGE_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "low_badge_url=$LOW_BADGE_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "version_badge_url=$VERSION_BADGE_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "latest_release_badge_url=$LATEST_RELEASE_BADGE_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "project_url=$PROJECT_URL" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
@@ -228,11 +374,29 @@ if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
**Progress:** ${MUSTDO_DONE} of ${MUSTDO_TOTAL} completed (${MUSTDO_PERCENTAGE}%)
|
||||
**Badge Color:** ${MUSTDO_COLOR}
|
||||
|
||||
### Medium Priority
|
||||
**Progress:** ${MEDIUM_DONE} of ${MEDIUM_TOTAL} completed (${MEDIUM_PERCENTAGE}%)
|
||||
**Badge Color:** ${MEDIUM_COLOR}
|
||||
|
||||
### Low Priority
|
||||
**Progress:** ${LOW_DONE} of ${LOW_TOTAL} completed (${LOW_PERCENTAGE}%)
|
||||
**Badge Color:** ${LOW_COLOR}
|
||||
|
||||
**Gist ID:** ${GIST_ID}
|
||||
|
||||
### Next Release
|
||||
**Version:** ${VERSION_MESSAGE}
|
||||
|
||||
### Latest Release
|
||||
**Release:** ${LATEST_RELEASE_MESSAGE}
|
||||
|
||||
### Badge URLs
|
||||
**Critical Priority Markdown:** \`[](${PROJECT_URL})\`
|
||||
**High Priority Markdown:** \`[](${PROJECT_URL})\`
|
||||
**Medium Priority Markdown:** \`[](${PROJECT_URL})\`
|
||||
**Low Priority Markdown:** \`[](${PROJECT_URL})\`
|
||||
**Next Release Markdown:** \`[](${PROJECT_URL})\`
|
||||
**Latest Release Markdown:** \`[](${RELEASES_URL})\`
|
||||
|
||||
### Badge JSON Preview
|
||||
\`\`\`json
|
||||
@@ -241,6 +405,18 @@ ${BLOCKER_BADGE_JSON}
|
||||
|
||||
// High Priority Badge
|
||||
${MUSTDO_BADGE_JSON}
|
||||
|
||||
// Medium Priority Badge
|
||||
${MEDIUM_BADGE_JSON}
|
||||
|
||||
// Low Priority Badge
|
||||
${LOW_BADGE_JSON}
|
||||
|
||||
// Next Release Badge
|
||||
${VERSION_BADGE_JSON}
|
||||
|
||||
// Latest Release Badge
|
||||
${LATEST_RELEASE_BADGE_JSON}
|
||||
\`\`\`
|
||||
EOF
|
||||
fi
|
||||
|
||||
@@ -28,6 +28,8 @@ FIELD_STATUS = "Status"
|
||||
# Expected values for Priority field
|
||||
VALUE_CRITICAL = "P0 - Critical"
|
||||
VALUE_HIGH = "P1 - High"
|
||||
VALUE_MEDIUM = "P2 - Medium"
|
||||
VALUE_LOW = "P3 - Low"
|
||||
|
||||
# Expected value for Status field when an item is completed
|
||||
VALUE_STATUS_DONE = "Done"
|
||||
@@ -129,16 +131,21 @@ class GitHubProjectTracker:
|
||||
|
||||
def fetch_release_blocker_stats(self) -> Dict[str, int]:
|
||||
"""
|
||||
Fetches critical and high priority issue statistics from the GitHub project.
|
||||
Fetches critical, high, medium, and low priority issue statistics from the GitHub project.
|
||||
|
||||
Returns:
|
||||
Dict with 'total', 'done', 'percentage', 'blocker_total', 'blocker_done',
|
||||
'mustdo_total', 'mustdo_done' keys
|
||||
'mustdo_total', 'mustdo_done', 'medium_total', 'medium_done',
|
||||
'low_total', 'low_done' keys
|
||||
"""
|
||||
blocker_total = 0
|
||||
blocker_done = 0
|
||||
mustdo_total = 0
|
||||
mustdo_done = 0
|
||||
medium_total = 0
|
||||
medium_done = 0
|
||||
low_total = 0
|
||||
low_done = 0
|
||||
cursor = None
|
||||
|
||||
# Track if we've seen the expected fields at least once
|
||||
@@ -206,6 +213,14 @@ class GitHubProjectTracker:
|
||||
mustdo_total += 1
|
||||
if status == VALUE_STATUS_DONE:
|
||||
mustdo_done += 1
|
||||
elif priority == VALUE_MEDIUM:
|
||||
medium_total += 1
|
||||
if status == VALUE_STATUS_DONE:
|
||||
medium_done += 1
|
||||
elif priority == VALUE_LOW:
|
||||
low_total += 1
|
||||
if status == VALUE_STATUS_DONE:
|
||||
low_done += 1
|
||||
|
||||
# Check for next page
|
||||
page_info = items.get("pageInfo", {})
|
||||
@@ -218,7 +233,7 @@ class GitHubProjectTracker:
|
||||
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 critical and high priority items.",
|
||||
print("This field is required to identify priority items.",
|
||||
file=sys.stderr)
|
||||
print("Possible causes:", file=sys.stderr)
|
||||
print(f" 1. Field '{FIELD_PRIORITY}' was renamed in the project", file=sys.stderr)
|
||||
@@ -238,22 +253,22 @@ class GitHubProjectTracker:
|
||||
print("Action required: Update FIELD_STATUS constant in this script.", file=sys.stderr)
|
||||
raise ProjectFieldMissingError(f"Critical field '{FIELD_STATUS}' not found")
|
||||
|
||||
total = blocker_total + mustdo_total
|
||||
done = blocker_done + mustdo_done
|
||||
total = blocker_total + mustdo_total + medium_total + low_total
|
||||
done = blocker_done + mustdo_done + medium_done + low_done
|
||||
|
||||
# Validate that we found at least some items
|
||||
# If total is 0, either the project is empty or field matching failed
|
||||
if total == 0:
|
||||
if self.milestone_filter:
|
||||
print(f"INFO: No critical or high priority items found for milestone '{self.milestone_filter}'.", file=sys.stderr)
|
||||
print(f"INFO: No 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 critical or high priority items found (total=0).", file=sys.stderr)
|
||||
print("ERROR: No priority items found (total=0).", file=sys.stderr)
|
||||
print("This likely indicates:", 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(f" Expected values: '{VALUE_CRITICAL}', '{VALUE_HIGH}', '{VALUE_MEDIUM}', or '{VALUE_LOW}'", 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)
|
||||
@@ -268,6 +283,10 @@ class GitHubProjectTracker:
|
||||
'blocker_total': blocker_total,
|
||||
'blocker_done': blocker_done,
|
||||
'mustdo_total': mustdo_total,
|
||||
'medium_total': medium_total,
|
||||
'medium_done': medium_done,
|
||||
'low_total': low_total,
|
||||
'low_done': low_done,
|
||||
'mustdo_done': mustdo_done
|
||||
}
|
||||
|
||||
@@ -306,6 +325,84 @@ def get_hdf5_version_from_header(header_path: str = "../../src/H5public.h") -> O
|
||||
return None
|
||||
|
||||
|
||||
def get_latest_series_release(token: str, repo: str, major_version: str) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Finds the most recent published (non-draft, non-prerelease) GitHub Release
|
||||
whose tag belongs to the given major version series (e.g. "2" matches "2.1.1").
|
||||
|
||||
Returns a dict with 'tag' and 'date' (YYYY-MM-DD) keys, or None if no
|
||||
matching release was found or the API call failed. Failures here are
|
||||
non-fatal - this is purely informational, unlike the priority field checks.
|
||||
"""
|
||||
try:
|
||||
headers = {"Accept": "application/vnd.github.v3+json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.github.com/repos/{repo}/releases",
|
||||
headers=headers,
|
||||
params={"per_page": 30},
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
releases = response.json()
|
||||
|
||||
prefix = f"{major_version}."
|
||||
for release in releases:
|
||||
if release.get("draft") or release.get("prerelease"):
|
||||
continue
|
||||
tag = release.get("tag_name", "")
|
||||
if tag.startswith(prefix):
|
||||
published_at = release.get("published_at", "")
|
||||
return {
|
||||
"tag": tag,
|
||||
"date": published_at.split("T")[0] if published_at else ""
|
||||
}
|
||||
|
||||
print(f"INFO: No published releases found for series '{major_version}.x'", file=sys.stderr)
|
||||
return None
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f"Warning: Could not fetch latest release from GitHub API: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def get_milestone_due_date(token: str, repo: str, milestone_filter: str) -> Optional[str]:
|
||||
"""
|
||||
Finds the open GitHub milestone whose title contains milestone_filter
|
||||
(e.g. "2.2" matches "HDF5 2.2.0", the same substring match used for
|
||||
project item filtering) and returns its due date (YYYY-MM-DD), or None
|
||||
if no matching milestone exists or it has no due date set. Non-fatal.
|
||||
"""
|
||||
try:
|
||||
headers = {"Accept": "application/vnd.github.v3+json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.github.com/repos/{repo}/milestones",
|
||||
headers=headers,
|
||||
params={"state": "open", "per_page": 100},
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
milestones = response.json()
|
||||
|
||||
for milestone in milestones:
|
||||
title = milestone.get("title", "")
|
||||
if milestone_filter in title:
|
||||
due_on = milestone.get("due_on")
|
||||
return due_on.split("T")[0] if due_on else None
|
||||
|
||||
print(f"INFO: No open milestone matching '{milestone_filter}' found", file=sys.stderr)
|
||||
return None
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f"Warning: Could not fetch milestone due date from GitHub API: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run the tracker."""
|
||||
# Configuration - can be overridden by environment variables
|
||||
@@ -326,12 +423,42 @@ def main():
|
||||
else:
|
||||
print("No milestone filter - counting all release items", file=sys.stderr)
|
||||
|
||||
# Look up the most recent published release in the current major version series
|
||||
# (e.g. "2" -> "2.1.1"), independent of the project-board data above.
|
||||
REPO = os.getenv("GITHUB_REPOSITORY", "HDFGroup/hdf5")
|
||||
MAJOR_VERSION = MILESTONE_FILTER.split(".")[0] if MILESTONE_FILTER else None
|
||||
latest_release = get_latest_series_release(TOKEN, REPO, MAJOR_VERSION) if MAJOR_VERSION else None
|
||||
LATEST_RELEASE_TAG = latest_release["tag"] if latest_release else ""
|
||||
LATEST_RELEASE_DATE = latest_release["date"] if latest_release else ""
|
||||
|
||||
# Look up the target due date for the in-development milestone (e.g. "HDF5 2.2.0"),
|
||||
# to annotate the Next Release badge.
|
||||
MILESTONE_DUE_DATE = get_milestone_due_date(TOKEN, REPO, MILESTONE_FILTER) if MILESTONE_FILTER else None
|
||||
MILESTONE_DUE_DATE = MILESTONE_DUE_DATE or ""
|
||||
|
||||
# Write these independent of the project-board query below, so a project-board
|
||||
# failure doesn't discard release/milestone data that was already fetched.
|
||||
github_output = os.getenv("GITHUB_OUTPUT")
|
||||
if github_output:
|
||||
with open(github_output, "a") as f:
|
||||
f.write(f"version={MILESTONE_FILTER or 'all'}\n")
|
||||
f.write(f"latest_release_tag={LATEST_RELEASE_TAG}\n")
|
||||
f.write(f"latest_release_date={LATEST_RELEASE_DATE}\n")
|
||||
f.write(f"milestone_due_date={MILESTONE_DUE_DATE}\n")
|
||||
print(f"version={MILESTONE_FILTER or 'all'}")
|
||||
print(f"latest_release_tag={LATEST_RELEASE_TAG}")
|
||||
print(f"latest_release_date={LATEST_RELEASE_DATE}")
|
||||
print(f"milestone_due_date={MILESTONE_DUE_DATE}")
|
||||
if LATEST_RELEASE_TAG:
|
||||
print(f"Latest {MAJOR_VERSION}.x release: {LATEST_RELEASE_TAG} ({LATEST_RELEASE_DATE})")
|
||||
if MILESTONE_DUE_DATE:
|
||||
print(f"Milestone due date: {MILESTONE_DUE_DATE}")
|
||||
|
||||
try:
|
||||
tracker = GitHubProjectTracker(TOKEN, OWNER, PROJECT_NUMBER, MILESTONE_FILTER)
|
||||
stats = tracker.fetch_release_blocker_stats()
|
||||
|
||||
|
||||
# Output for GitHub Actions
|
||||
github_output = os.getenv("GITHUB_OUTPUT")
|
||||
if github_output:
|
||||
with open(github_output, "a") as f:
|
||||
f.write(f"percentage={stats['percentage']}\n")
|
||||
@@ -341,7 +468,10 @@ 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"version={MILESTONE_FILTER or 'all'}\n")
|
||||
f.write(f"medium_total={stats['medium_total']}\n")
|
||||
f.write(f"medium_done={stats['medium_done']}\n")
|
||||
f.write(f"low_total={stats['low_total']}\n")
|
||||
f.write(f"low_done={stats['low_done']}\n")
|
||||
|
||||
# Also output to stdout for local testing
|
||||
print(f"percentage={stats['percentage']}")
|
||||
@@ -349,14 +479,19 @@ 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"version={MILESTONE_FILTER or 'all'}")
|
||||
print(f"medium_done={stats['medium_done']}")
|
||||
print(f"medium_total={stats['medium_total']}")
|
||||
print(f"low_done={stats['low_done']}")
|
||||
print(f"low_total={stats['low_total']}")
|
||||
print(f"Calculated progress: {stats['percentage']}%")
|
||||
print(f"Done / Total: {stats['done']} / {stats['total']}")
|
||||
print(f"Critical Priority: {stats['blocker_done']} / {stats['blocker_total']}")
|
||||
print(f"High Priority: {stats['mustdo_done']} / {stats['mustdo_total']}")
|
||||
print(f"Medium Priority: {stats['medium_done']} / {stats['medium_total']}")
|
||||
print(f"Low Priority: {stats['low_done']} / {stats['low_total']}")
|
||||
if MILESTONE_FILTER:
|
||||
print(f"Milestone filter: {MILESTONE_FILTER}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@@ -115,12 +115,19 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract critical and high priority counts
|
||||
# Extract critical, high, medium, and low 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)
|
||||
MEDIUM_DONE=$(grep "^medium_done=" progress_output.txt | cut -d'=' -f2 | head -1)
|
||||
MEDIUM_TOTAL=$(grep "^medium_total=" progress_output.txt | cut -d'=' -f2 | head -1)
|
||||
LOW_DONE=$(grep "^low_done=" progress_output.txt | cut -d'=' -f2 | head -1)
|
||||
LOW_TOTAL=$(grep "^low_total=" progress_output.txt | cut -d'=' -f2 | head -1)
|
||||
VERSION=$(grep "^version=" progress_output.txt | cut -d'=' -f2 | head -1)
|
||||
LATEST_RELEASE_TAG=$(grep "^latest_release_tag=" progress_output.txt | cut -d'=' -f2 | head -1)
|
||||
LATEST_RELEASE_DATE=$(grep "^latest_release_date=" progress_output.txt | cut -d'=' -f2 | head -1)
|
||||
MILESTONE_DUE_DATE=$(grep "^milestone_due_date=" progress_output.txt | cut -d'=' -f2 | head -1)
|
||||
|
||||
# Set outputs for use in subsequent steps
|
||||
echo "percentage=$PERCENTAGE" >> $GITHUB_OUTPUT
|
||||
@@ -130,10 +137,18 @@ jobs:
|
||||
echo "blocker_total=$BLOCKER_TOTAL" >> $GITHUB_OUTPUT
|
||||
echo "mustdo_done=$MUSTDO_DONE" >> $GITHUB_OUTPUT
|
||||
echo "mustdo_total=$MUSTDO_TOTAL" >> $GITHUB_OUTPUT
|
||||
echo "medium_done=$MEDIUM_DONE" >> $GITHUB_OUTPUT
|
||||
echo "medium_total=$MEDIUM_TOTAL" >> $GITHUB_OUTPUT
|
||||
echo "low_done=$LOW_DONE" >> $GITHUB_OUTPUT
|
||||
echo "low_total=$LOW_TOTAL" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "latest_release_tag=$LATEST_RELEASE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "latest_release_date=$LATEST_RELEASE_DATE" >> $GITHUB_OUTPUT
|
||||
echo "milestone_due_date=$MILESTONE_DUE_DATE" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "::notice::Progress calculation successful: ${PERCENTAGE}% (${DONE}/${TOTAL})"
|
||||
echo "::notice:: Version: ${VERSION}, Critical Priority: ${BLOCKER_DONE}/${BLOCKER_TOTAL}, High Priority: ${MUSTDO_DONE}/${MUSTDO_TOTAL}"
|
||||
echo "::notice:: Version: ${VERSION}, Critical Priority: ${BLOCKER_DONE}/${BLOCKER_TOTAL}, High Priority: ${MUSTDO_DONE}/${MUSTDO_TOTAL}, Medium Priority: ${MEDIUM_DONE}/${MEDIUM_TOTAL}, Low Priority: ${LOW_DONE}/${LOW_TOTAL}"
|
||||
echo "::notice:: Latest release: ${LATEST_RELEASE_TAG} (${LATEST_RELEASE_DATE}), Milestone due date: ${MILESTONE_DUE_DATE}"
|
||||
|
||||
# Clean up
|
||||
rm -f progress_output.txt
|
||||
@@ -154,7 +169,14 @@ jobs:
|
||||
BLOCKER_TOTAL: ${{ steps.progress.outputs.blocker_total }}
|
||||
MUSTDO_DONE: ${{ steps.progress.outputs.mustdo_done }}
|
||||
MUSTDO_TOTAL: ${{ steps.progress.outputs.mustdo_total }}
|
||||
MEDIUM_DONE: ${{ steps.progress.outputs.medium_done }}
|
||||
MEDIUM_TOTAL: ${{ steps.progress.outputs.medium_total }}
|
||||
LOW_DONE: ${{ steps.progress.outputs.low_done }}
|
||||
LOW_TOTAL: ${{ steps.progress.outputs.low_total }}
|
||||
VERSION: ${{ steps.progress.outputs.version }}
|
||||
LATEST_RELEASE_TAG: ${{ steps.progress.outputs.latest_release_tag }}
|
||||
LATEST_RELEASE_DATE: ${{ steps.progress.outputs.latest_release_date }}
|
||||
MILESTONE_DUE_DATE: ${{ steps.progress.outputs.milestone_due_date }}
|
||||
run: |
|
||||
# Execute dedicated badge generation script
|
||||
# This separates concerns: YAML orchestrates, scripts implement logic
|
||||
@@ -172,6 +194,8 @@ jobs:
|
||||
REPO_NAME="${GITHUB_REPOSITORY##*/}"
|
||||
BLOCKER_GIST_NAME="release-blocker-${REPO_NAME}.json"
|
||||
MUSTDO_GIST_NAME="release-mustdo-${REPO_NAME}.json"
|
||||
MEDIUM_GIST_NAME="release-medium-${REPO_NAME}.json"
|
||||
LOW_GIST_NAME="release-low-${REPO_NAME}.json"
|
||||
|
||||
FAILURE_BADGE=$(jq -n '{
|
||||
"schemaVersion": 1,
|
||||
@@ -184,11 +208,15 @@ jobs:
|
||||
REQUEST_PAYLOAD=$(jq -n \
|
||||
--arg blocker_filename "$BLOCKER_GIST_NAME" \
|
||||
--arg mustdo_filename "$MUSTDO_GIST_NAME" \
|
||||
--arg medium_filename "$MEDIUM_GIST_NAME" \
|
||||
--arg low_filename "$LOW_GIST_NAME" \
|
||||
--argjson content "$FAILURE_BADGE" \
|
||||
'{
|
||||
"files": {
|
||||
($blocker_filename): { "content": ($content | tostring) },
|
||||
($mustdo_filename): { "content": ($content | tostring) }
|
||||
($mustdo_filename): { "content": ($content | tostring) },
|
||||
($medium_filename): { "content": ($content | tostring) },
|
||||
($low_filename): { "content": ($content | tostring) }
|
||||
}
|
||||
}')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user