diff --git a/.github/workflows/update-progress.py b/.github/workflows/update-progress.py new file mode 100644 index 00000000000..45c6b93f37b --- /dev/null +++ b/.github/workflows/update-progress.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +GitHub Project Release Blocker Progress Tracker +Fetches release blocker issues from the HDF5 project and calculates completion percentage. +""" + +import requests +from typing import Dict, Any, Optional + +class GitHubProjectTracker: + """Tracks release blocker progress in GitHub projects.""" + + def __init__(self, token: str, owner: str, project_number: int): + self.api_url = "https://api.github.com/graphql" + self.headers = { + "Authorization": f"bearer {token}", + "Content-Type": "application/json" + } + self.owner = owner + self.project_number = project_number + + def _get_query(self) -> str: + """Returns the GraphQL query for fetching project items.""" + return """ + query($owner: String!, $projectNumber: Int!, $cursor: String) { + organization(login: $owner) { + projectV2(number: $projectNumber) { + items(first: 100, after: $cursor) { + pageInfo { hasNextPage, endCursor } + nodes { + id + fieldValues(first: 20) { + nodes { + __typename + ... on ProjectV2ItemFieldTextValue { + text, field { ... on ProjectV2Field { name } } + } + ... on ProjectV2ItemFieldSingleSelectValue { + name, field { ... on ProjectV2SingleSelectField { name } } + } + ... on ProjectV2ItemFieldIterationValue { + title, field { ... on ProjectV2IterationField { name } } + } + ... on ProjectV2ItemFieldNumberValue { + number, field { ... on ProjectV2Field { name } } + } + ... on ProjectV2ItemFieldDateValue { + date, field { ... on ProjectV2Field { name } } + } + } + } + content { ... on Issue { id, title, url } } + } + } + } + } + } + """ + + def _extract_field_value(self, field_data: Dict[str, Any]) -> Optional[str]: + """Extracts value from a field based on its type.""" + type_name = field_data.get("__typename") + + value_map = { + "ProjectV2ItemFieldSingleSelectValue": "name", + "ProjectV2ItemFieldIterationValue": "title", + "ProjectV2ItemFieldTextValue": "text", + "ProjectV2ItemFieldNumberValue": "number", + "ProjectV2ItemFieldDateValue": "date" + } + + value_key = value_map.get(type_name) + return field_data.get(value_key) if value_key else None + + def _parse_item_fields(self, item: Dict[str, Any]) -> Dict[str, str]: + """Parses field values from a project item.""" + fields = {} + + for field_data in item.get("fieldValues", {}).get("nodes", []): + field_name = field_data.get("field", {}).get("name") + if not field_name: + continue + + value = self._extract_field_value(field_data) + if value is not None: + fields[field_name] = str(value) + + return fields + + def fetch_release_blocker_stats(self) -> Dict[str, int]: + """ + Fetches release blocker statistics from the GitHub project. + + Returns: + Dict with 'total', 'done', and 'percentage' keys + """ + total = 0 + done = 0 + cursor = None + + while True: + variables = { + "owner": self.owner, + "projectNumber": self.project_number, + "cursor": cursor + } + + try: + response = requests.post( + self.api_url, + json={'query': self._get_query(), 'variables': variables}, + headers=self.headers, + timeout=30 + ) + response.raise_for_status() + result = response.json() + + if "errors" in result: + raise Exception(f"GraphQL errors: {result['errors']}") + + except requests.RequestException as e: + raise Exception(f"API request failed: {e}") + + # Parse response + project = result.get("data", {}).get("organization", {}).get("projectV2", {}) + items = project.get("items", {}) + + for item in items.get("nodes", []): + if not item.get("content"): + continue + + fields = self._parse_item_fields(item) + + if fields.get("Release gating") == "Release_Blocker": + total += 1 + if fields.get("Status") == "Done": + done += 1 + + # Check for next page + page_info = items.get("pageInfo", {}) + if not page_info.get("hasNextPage", False): + break + cursor = page_info.get("endCursor") + + percentage = round((done / total * 100), 1) if total > 0 else 0 + + return { + 'total': total, + 'done': done, + 'percentage': percentage + } + + +def main(): + """Main function to run the tracker.""" + import os + import sys + + # Configuration - can be overridden by environment variables + TOKEN = os.getenv("GITHUB_TOKEN") + OWNER = os.getenv("GITHUB_OWNER", "HDFGroup") + PROJECT_NUMBER = int(os.getenv("GITHUB_PROJECT_NUMBER", "39")) + + try: + tracker = GitHubProjectTracker(TOKEN, OWNER, PROJECT_NUMBER) + 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") + f.write(f"done={stats['done']}\n") + f.write(f"total={stats['total']}\n") + + # Also output to stdout for local testing + print(f"percentage={stats['percentage']}") + print(f"Calculated progress: {stats['percentage']}%") + print(f"Done / Total: {stats['done']} / {stats['total']}") + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/.github/workflows/update-progress.yml b/.github/workflows/update-progress.yml new file mode 100644 index 00000000000..a00eec56169 --- /dev/null +++ b/.github/workflows/update-progress.yml @@ -0,0 +1,250 @@ +name: Release Progress + +on: + schedule: + - cron: '0 */24 * * *' # Run every 24 hours + workflow_dispatch: # Allow manual triggering + +jobs: + check-progress: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' # Use specific version for consistency + + - name: Cache pip dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + pip install requests + + - name: Validate required secrets + run: | + if [ -z "${{ secrets.GIST_TOKEN }}" ]; then + echo "::error::GIST_TOKEN secret is required" + exit 1 + fi + if [ -z "${{ secrets.GIST_ID }}" ]; then + echo "::error::GIST_ID secret is required" + exit 1 + fi + echo "::notice::All required secrets are present" + + - name: Debug information + if: runner.debug == '1' + run: | + echo "Repository: $GITHUB_REPOSITORY" + echo "Workflow: $GITHUB_WORKFLOW" + echo "Run ID: $GITHUB_RUN_ID" + echo "Python version: $(python --version)" + echo "Environment variables:" + env | grep -E '^GITHUB_' | sort + + - name: Calculate progress (run once) + id: progress + run: | + # Check if Python script exists + if [ ! -f "update-progress.py" ]; then + echo "::error::update-progress.py not found in repository root" + exit 1 + fi + + echo "::notice::Running progress calculation script..." + + # Run the Python script with error handling + if ! python update-progress.py > progress_output.txt 2>&1; then + echo "::error::Python script execution failed" + echo "Script output:" + cat progress_output.txt + exit 1 + fi + + # Display the captured output for debugging + echo "=== Python Script Output ===" + cat progress_output.txt + echo "==========================" + + # Extract and validate percentage + PERCENTAGE=$(grep "^percentage=" progress_output.txt | cut -d'=' -f2 | head -1) + if [ -z "$PERCENTAGE" ] || ! [[ "$PERCENTAGE" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + echo "::error::Invalid or missing percentage value: '$PERCENTAGE'" + echo "Expected format: percentage=XX.X" + exit 1 + fi + + # Extract and validate done/total counts + DONE_TOTAL=$(grep "^Done / Total:" progress_output.txt | cut -d':' -f2 | xargs | head -1) + if [ -z "$DONE_TOTAL" ]; then + echo "::error::Missing 'Done / Total:' line in script output" + exit 1 + fi + + DONE=$(echo "$DONE_TOTAL" | cut -d' ' -f1) + TOTAL=$(echo "$DONE_TOTAL" | cut -d' ' -f3) # Account for "XX / YY" format + + # Validate numeric values + if ! [[ "$DONE" =~ ^[0-9]+$ ]] || ! [[ "$TOTAL" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid done/total values: done='$DONE', total='$TOTAL'" + echo "Expected format: 'Done / Total: XX / YY'" + exit 1 + fi + + # Validate logical consistency + if [ "$TOTAL" -eq 0 ]; then + echo "::error::Total count cannot be zero" + exit 1 + fi + + if [ "$DONE" -gt "$TOTAL" ]; then + echo "::error::Done count ($DONE) cannot exceed total count ($TOTAL)" + exit 1 + fi + + # Set outputs for use in subsequent steps + echo "percentage=$PERCENTAGE" >> $GITHUB_OUTPUT + echo "done=$DONE" >> $GITHUB_OUTPUT + echo "total=$TOTAL" >> $GITHUB_OUTPUT + + echo "::notice::Progress calculation successful: ${PERCENTAGE}% (${DONE}/${TOTAL})" + + # Clean up + rm -f progress_output.txt + + env: + GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }} + GITHUB_OWNER: "HDFGroup" + GITHUB_PROJECT_NUMBER: "39" + + - name: Update progress badge Gist + env: + GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }} + GIST_ID: ${{ secrets.GIST_ID }} + run: | + PERCENTAGE="${{ steps.progress.outputs.percentage }}" + DONE="${{ steps.progress.outputs.done }}" + TOTAL="${{ steps.progress.outputs.total }}" + + echo "::notice::Updating badge with: ${PERCENTAGE}% (${DONE}/${TOTAL})" + + # Use integer comparison for more reliable thresholds + PERCENTAGE_INT=$(printf "%.0f" "$PERCENTAGE") + + # Determine badge color and status with cleaner logic + if [ "$PERCENTAGE_INT" -ge 90 ]; then + COLOR="brightgreen" + STATUS="🟢 Readying for Deployment" + elif [ "$PERCENTAGE_INT" -ge 60 ]; then + COLOR="yellow" + STATUS="🟡 Nearing Completion" + elif [ "$PERCENTAGE_INT" -ge 40 ]; then + COLOR="orange" + STATUS="🟠 In Development" + else + COLOR="red" + STATUS="🔴 Initial Phase" + fi + + echo "::notice title=Release Progress::${PERCENTAGE}% Complete (${DONE}/${TOTAL}) - ${STATUS}" + + # Create badge JSON for shields.io endpoint with proper escaping + BADGE_JSON=$(jq -n \ + --arg percentage "$PERCENTAGE" \ + --arg done "$DONE" \ + --arg total "$TOTAL" \ + --arg color "$COLOR" \ + '{ + "schemaVersion": 1, + "label": "Release Progress", + "message": "\($percentage)% (\($done)/\($total))", + "color": $color, + "style": "flat-square" + }') + + # Validate JSON was created successfully + if [ -z "$BADGE_JSON" ] || ! echo "$BADGE_JSON" | jq empty 2>/dev/null; then + echo "::error::Failed to generate valid badge JSON" + exit 1 + fi + + # The filename in the Gist must match the one created manually + GIST_NAME="release-progress-${GITHUB_REPOSITORY##*/}.json" + echo "::notice::Updating Gist file: $GIST_NAME" + + # Create the request payload + REQUEST_PAYLOAD=$(jq -n \ + --arg filename "$GIST_NAME" \ + --argjson content "$BADGE_JSON" \ + '{ + "files": { + ($filename): { + "content": ($content | tostring) + } + } + }') + + # Update the existing Gist with response validation + echo "Updating Gist: ${GIST_ID}" + RESPONSE=$(curl -s -w "\n%{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") + + # Extract HTTP status code (last line) and response body + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + RESPONSE_BODY=$(echo "$RESPONSE" | head -n -1) + + # Validate API response + if [ "$HTTP_CODE" != "200" ]; then + echo "::error::Gist update failed with HTTP status $HTTP_CODE" + echo "Response body: $RESPONSE_BODY" + exit 1 + fi + + echo "::notice::Gist updated successfully" + + # Generate badge URL for use in README + BADGE_URL="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/${GITHUB_REPOSITORY_OWNER}/${GIST_ID}/raw/${GIST_NAME}" + PROJECT_URL="https://github.com/${GITHUB_REPOSITORY}/projects/39" + + echo "::notice::Badge URL generated: $BADGE_URL" + echo "badge_url=$BADGE_URL" >> $GITHUB_OUTPUT + echo "project_url=$PROJECT_URL" >> $GITHUB_OUTPUT + + # Create enhanced step summary + echo "## 📊 Release Progress: ${PERCENTAGE}%" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** ${STATUS}" >> $GITHUB_STEP_SUMMARY + echo "**Progress:** ${DONE} of ${TOTAL} release blockers completed" >> $GITHUB_STEP_SUMMARY + echo "**Badge Color:** ${COLOR}" >> $GITHUB_STEP_SUMMARY + echo "**Gist ID:** ${GIST_ID}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Badge URLs" >> $GITHUB_STEP_SUMMARY + echo "**Markdown:** \`[![Release Progress](${BADGE_URL})](${PROJECT_URL})\`" >> $GITHUB_STEP_SUMMARY + echo "**Image Only:** \`![Release Progress](${BADGE_URL})\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Badge JSON Preview" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + echo "$BADGE_JSON" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + - name: Cleanup on failure + if: failure() + run: | + echo "::warning::Workflow failed - cleaning up temporary files" + rm -f progress_output.txt + echo "::notice::Check the logs above for specific error details" diff --git a/README.md b/README.md index b3e84a762ff..fbf488d49ed 100644 --- a/README.md +++ b/README.md @@ -88,19 +88,24 @@ RELEASE SCHEDULE HDF5 does not follow a regular release schedule. Instead, updates are based on the introduction of new features and the resolution of bugs. However, we aim to have at -least one annual release for each maintenance branch. Please note that the future -HDF5 releases listed on this schedule are tentative. +least one annual release for each maintenance branch. | Release | New Features | | ------- | ------------ | | 2.0.0 | Drop Autotools support, drop the HDF5 <--> GIF tools, add complex number support, update library defaults (cache sizes, etc.) | | FUTURE | Multi-threaded HDF5, crashproofing / metadata journaling, Full (VFD) SWMR, encryption, digital signatures, sparse datasets, improved storage for variable-length datatypes, better Unicode support (especially on Windows) | -[A list of planned HDF5 2.0 features and bugfixes can be found here.](https://github.com/orgs/HDFGroup/projects/39/views/14) +### Release Progress +[![Release Progress](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/HDFGroup/84df5ca83b993784306ac2f3fd12dc3c/raw/release-progress-hdf5.json)](https://github.com/orgs/HDFGroup/projects/39/views/24) -This list of feature release versions is tentative, and the release -in which a feature is introduced may change. +The badge above shows the current progress of release-blocking issues with colors that reflect completion status: +- **🟢 Green (90%+)**: Readying for Deployment - most blockers completed +- **🟡 Yellow (60-89%)**: Nearing Completion - on track for release +- **🟠 Orange (40-59%)**: In Development - attention needed +- **🔴 Red (<40%)**: Initial Phase - significant blockers remain + +Click the badge to view the detailed project board with current release-blocking issues. SNAPSHOTS, PREVIOUS RELEASES AND SOURCE CODE --------------------------------------------