-
Notifications
You must be signed in to change notification settings - Fork 728
Add more validation and automation to the maintainer updates #1179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ff8046a
Add more validation and automation to the maintainer updates
krook f0736d4
Update .github/workflows/validate-maintainers.yml
krook 4502ec0
Update .github/workflows/validate-maintainers.yml
krook c2d6eae
Update .github/workflows/validate-maintainers.yml
krook 1576afe
Update .github/workflows/validate-maintainers.yml
krook File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| ### Pre-submission checklist for maintainer updates (delete this if you're updating a different file) | ||
| # Checklist for maintainer updates | ||
|
|
||
| _If you're adding a new maintainer to the CSV file, please review each of these actions as well:_ | ||
| > [!NOTE] | ||
| > **Delete this template if you're not changing the CSV file** | ||
|
|
||
| - [ ] You've provided a link to documentation where the project has approved the maintainer changes. | ||
| - [ ] The maintainer(s) also created or updated their [LFX Individual Dashboard profile](https://openprofile.dev/). | ||
| - [ ] You've sent an email with the email address(es) to <[email protected]> for invitations to Service Desk and mailing lists. | ||
| - [ ] You've sent an email with the list of email address(es) to <[email protected]> for invitations to Service Desk and mailing lists. You can just mark this complete if you are only removing people. | ||
| - [ ] Optional: You've also sent a PR with affiliation updates to [cncf/gitdm](https://github.com/cncf/gitdm?tab=readme-ov-file#cncf-gitdm). |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,271 @@ | ||
| name: Maintainer PR Validation | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, edited, ready_for_review, synchronize] | ||
| paths: | ||
| - "project-maintainers.csv" | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| issues: write | ||
|
|
||
| jobs: | ||
| validate-maintainers: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 2 | ||
|
|
||
| - name: Check PR requirements | ||
| id: check-requirements | ||
| run: | | ||
| # Create a Python script to safely parse the PR body | ||
| cat > check_requirements.py << 'EOF' | ||
| import os | ||
| import re | ||
|
|
||
| # Get PR body from environment | ||
| pr_body = os.getenv('PR_BODY', '') | ||
|
|
||
| # Debug: Print first 200 chars of PR body | ||
| print(f"PR body preview: {repr(pr_body[:200])}") | ||
|
|
||
| # Find all checkboxes in order (both checked and unchecked) | ||
| # Pattern matches: - [ ] or - [x] or - [X] | ||
| all_checkboxes = re.findall(r'- \[([xX ])\]', pr_body, re.MULTILINE) | ||
|
|
||
| print(f"Found {len(all_checkboxes)} total checkboxes: {all_checkboxes}") | ||
|
|
||
| # Check if first 3 checkboxes are completed | ||
| check_results = [] | ||
| for i in range(3): # Check first 3 checkboxes | ||
| if i < len(all_checkboxes): | ||
| is_checked = all_checkboxes[i].lower() == 'x' | ||
| check_results.append(is_checked) | ||
| print(f"Checkbox {i+1}: {'CHECKED' if is_checked else 'UNCHECKED'} ({all_checkboxes[i]})") | ||
| else: | ||
| # Not enough checkboxes found | ||
| check_results.append(False) | ||
| print(f"Checkbox {i+1}: NOT FOUND") | ||
|
|
||
| all_complete = all(check_results) | ||
| print(f"All first three checkboxes complete: {all_complete}") | ||
|
|
||
| # Write to GitHub output file only | ||
| github_output = os.environ.get('GITHUB_OUTPUT') | ||
| if github_output: | ||
| with open(github_output, 'a') as f: | ||
| for i, check in enumerate(check_results, 1): | ||
| f.write(f"check{i}={'true' if check else 'false'}\n") | ||
| f.write(f"all_required_complete={'true' if all_complete else 'false'}\n") | ||
| EOF | ||
|
|
||
| # Run the Python script | ||
| python3 check_requirements.py | ||
| env: | ||
| PR_BODY: ${{ github.event.pull_request.body }} | ||
|
|
||
| - name: Add warning comment if requirements not met | ||
| if: steps.check-requirements.outputs.all_required_complete == 'false' | ||
| run: | | ||
| # Add comment explaining requirements | ||
| gh pr comment ${{ github.event.pull_request.number }} --body "⚠️ **PR Requirements Not Met** | ||
|
|
||
| This PR can't be merged until the first three checkboxes are marked complete." | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Add approval comment if requirements met | ||
| if: steps.check-requirements.outputs.all_required_complete == 'true' | ||
| run: | | ||
| gh pr comment ${{ github.event.pull_request.number }} --body "✅ **PR Requirements Complete** | ||
|
|
||
| All required checkboxes have been completed. This PR is ready to handle and merge." | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Analyze CSV changes | ||
| if: steps.check-requirements.outputs.all_required_complete == 'true' | ||
| id: analyze-changes | ||
| run: | | ||
| # Get the changes in the CSV file | ||
| git diff HEAD~1 HEAD project-maintainers.csv > csv_changes.diff | ||
|
|
||
| # Parse the CSV changes and create the change summary | ||
| python3 << 'EOF' | ||
| import csv | ||
| import sys | ||
| import os | ||
|
|
||
| def parse_csv_file(filename): | ||
| """Parse CSV file and return list of rows with project inheritance""" | ||
| rows = [] | ||
| current_project = "" | ||
| try: | ||
| with open(filename, 'r', newline='', encoding='utf-8') as csvfile: | ||
| reader = csv.reader(csvfile) | ||
| for row in reader: | ||
| if len(row) >= 4: # Ensure minimum columns | ||
| # Inherit project from most recent non-empty project row | ||
| if len(row) > 1 and row[1].strip(): # Project column is not empty | ||
| current_project = row[1].strip() | ||
| elif current_project: # Project column is empty, use inherited | ||
| # Create a copy of the row with inherited project | ||
| row_copy = row.copy() | ||
| if len(row_copy) > 1: | ||
| row_copy[1] = current_project | ||
| row = row_copy | ||
| rows.append(row) | ||
| except FileNotFoundError: | ||
| return [] | ||
| return rows | ||
|
|
||
| def create_change_summary(): | ||
| """Analyze changes and create summary""" | ||
| # Get current CSV content | ||
| current_rows = parse_csv_file('project-maintainers.csv') | ||
|
|
||
| # Try to get previous version | ||
| os.system('git show HEAD~1:project-maintainers.csv > previous.csv 2>/dev/null || touch previous.csv') | ||
krook marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| previous_rows = parse_csv_file('previous.csv') | ||
|
|
||
| changes = [] | ||
|
|
||
| # Create lookup dictionaries for comparison | ||
| # Using combination of project and maintainer name as key | ||
| def make_key(row): | ||
| if len(row) >= 2: | ||
| return f"{row[1]}|{row[2]}" if len(row) > 2 else f"{row[1]}|" | ||
krook marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return "" | ||
|
|
||
| previous_dict = {make_key(row): row for row in previous_rows if len(row) >= 2} | ||
| current_dict = {make_key(row): row for row in current_rows if len(row) >= 2} | ||
|
|
||
| # Find additions and updates | ||
| for key, current_row in current_dict.items(): | ||
| if len(current_row) >= 4: | ||
| if key not in previous_dict: | ||
| # New entry | ||
| changes.append({ | ||
| 'project': current_row[1], | ||
| 'maintainer': current_row[2], | ||
| 'company': current_row[3], | ||
| 'github': current_row[4] if len(current_row) > 4 else '', | ||
| 'change': 'Add' | ||
| }) | ||
| elif current_row != previous_dict[key]: | ||
| # Updated entry | ||
| changes.append({ | ||
| 'project': current_row[1], | ||
| 'maintainer': current_row[2], | ||
| 'company': current_row[3], | ||
| 'github': current_row[4] if len(current_row) > 4 else '', | ||
| 'change': 'Update' | ||
| }) | ||
|
|
||
| # Find removals | ||
| for key, previous_row in previous_dict.items(): | ||
| if key not in current_dict and len(previous_row) >= 4: | ||
| changes.append({ | ||
| 'project': previous_row[1], | ||
| 'maintainer': previous_row[2], | ||
| 'company': previous_row[3], | ||
| 'github': previous_row[4] if len(previous_row) > 4 else '', | ||
| 'change': 'Remove' | ||
| }) | ||
|
|
||
| return changes | ||
|
|
||
| changes = create_change_summary() | ||
|
|
||
| # Create the output CSV content | ||
| csv_content = "Project,Maintainer Name,Company,Email,GitHub Handle,Change\n" | ||
| for change in changes: | ||
| csv_content += f"{change['project']},{change['maintainer']},{change['company']},,{change['github']},{change['change']}\n" | ||
|
|
||
| # Write to file for email | ||
| with open('changes_summary.csv', 'w') as f: | ||
| f.write(csv_content) | ||
|
|
||
krook marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # Set output for GitHub Actions | ||
| print(f"Found {len(changes)} changes") | ||
| with open(os.environ['GITHUB_OUTPUT'], 'a') as f: | ||
| f.write(f"changes_count={len(changes)}\n") | ||
| f.write(f"has_changes={'true' if changes else 'false'}\n") | ||
| EOF | ||
|
|
||
| - name: Add changes comment | ||
| if: steps.analyze-changes.outputs.has_changes == 'true' && steps.check-requirements.outputs.all_required_complete == 'true' | ||
| run: | | ||
| # Create Markdown table from changes summary | ||
| python3 << 'EOF' | ||
| import csv | ||
| import os | ||
|
|
||
| def create_markdown_table(): | ||
| """Convert CSV changes to Markdown table""" | ||
| if not os.path.exists('changes_summary.csv'): | ||
| return "No changes detected." | ||
|
|
||
| with open('changes_summary.csv', 'r') as f: | ||
| reader = csv.reader(f) | ||
| rows = list(reader) | ||
|
|
||
| if len(rows) <= 1: # Only header or empty | ||
| return "No changes detected." | ||
|
|
||
| # Create Markdown table | ||
| table = "| Project | Maintainer Name | Company | Email | GitHub Handle | Change |\n" | ||
| table += "|---------|-----------------|---------|-------|---------------|--------|\n" | ||
|
|
||
| for row in rows[1:]: # Skip header | ||
| if len(row) >= 6: | ||
| # Escape pipe characters in data and handle empty values | ||
| project = row[0].replace('|', '\\|') if row[0] else '-' | ||
| maintainer = row[1].replace('|', '\\|') if row[1] else '-' | ||
| company = row[2].replace('|', '\\|') if row[2] else '-' | ||
| email = row[3].replace('|', '\\|') if row[3] else '_Shared by email_' | ||
| github = row[4].replace('|', '\\|') if row[4] else '-' | ||
| change = row[5].replace('|', '\\|') if row[5] else '-' | ||
|
|
||
| # Add emoji for change type | ||
| change_emoji = { | ||
| 'Add': '✅ Add', | ||
| 'Update': '🔄 Update', | ||
| 'Remove': '❌ Remove' | ||
| }.get(change, change) | ||
|
|
||
| table += f"| {project} | {maintainer} | {company} | {email} | {github} | {change_emoji} |\n" | ||
|
|
||
| return table | ||
|
|
||
| markdown_table = create_markdown_table() | ||
|
|
||
| # Write to file for use in next step | ||
| with open('changes_table.md', 'w') as f: | ||
| f.write(markdown_table) | ||
| EOF | ||
|
|
||
| # Read the generated table and post comment | ||
| CHANGES_TABLE=$(cat changes_table.md) | ||
|
|
||
| gh pr comment ${{ github.event.pull_request.number }} --body "## 📋 Maintainer Changes Summary | ||
|
|
||
| The following maintainer changes are ready handle: | ||
krook marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| $CHANGES_TABLE" | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Upload changes summary as artifact | ||
| if: steps.analyze-changes.outputs.has_changes == 'true' | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: maintainer-changes-${{ github.event.pull_request.number }} | ||
| path: | | ||
| changes_summary.csv | ||
| changes_table.md | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.