-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Implement PR quota workflow #7882
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 all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5aee0c9
Implement PR quota workflow
yurishkuro 47bca19
fix
yurishkuro b24aa06
fix
yurishkuro ded188b
fix
yurishkuro 3e6e488
fix
yurishkuro cde68d7
fix
yurishkuro 0845f81
fix
yurishkuro 5fb93f2
Apply suggestion from @Copilot
yurishkuro 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 |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| node_modules/ |
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,210 @@ | ||
| # PR Quota Manager - Manual Execution Guide | ||
|
|
||
| This document explains how to run the PR Quota Manager script manually from the command line for testing and troubleshooting. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| 1. **Node.js** (version 16 or higher) | ||
| ```bash | ||
| node --version | ||
| ``` | ||
|
|
||
| 2. **GitHub Personal Access Token** with the following permissions: | ||
| - `repo` (Full control of private repositories) | ||
| - `public_repo` (Access public repositories) - if working with public repos only | ||
|
|
||
| Create a token at: https://github.com/settings/tokens. | ||
| Store the value in a file, e.g. `~/.github_token`. | ||
| Then set the environment variable: | ||
| ```bash | ||
| read -r GITHUB_TOKEN < ~/.github_token | ||
| export GITHUB_TOKEN | ||
| ``` | ||
|
|
||
| 3. **Install Dependencies** | ||
|
|
||
| Navigate to the `.github/scripts` directory and install dependencies: | ||
| ```bash | ||
| cd .github/scripts | ||
| npm ci | ||
| ``` | ||
|
|
||
| ## Running the Script | ||
|
|
||
| ### Basic Usage | ||
|
|
||
| ```bash | ||
| node pr-quota-manager.js <username> [owner] [repo] | ||
| ``` | ||
|
|
||
| ### Parameters | ||
|
|
||
| - `username` (required): The GitHub username to process quota for | ||
| - `owner` (optional): Repository owner (defaults to `jaegertracing` or `GITHUB_REPOSITORY` env var) | ||
| - `repo` (optional): Repository name (defaults to `jaeger` or `GITHUB_REPOSITORY` env var) | ||
|
|
||
| ### Examples | ||
|
|
||
| **Process quota for a specific user in the default repository:** | ||
| ```bash | ||
| node pr-quota-manager.js newcontributor | ||
| ``` | ||
|
|
||
| **Process quota for a user in a different repository:** | ||
| ```bash | ||
| node pr-quota-manager.js username myorg myrepo | ||
| ``` | ||
|
|
||
| **Using environment variables for repository:** | ||
| ```bash | ||
| export GITHUB_REPOSITORY="jaegertracing/jaeger" | ||
| node pr-quota-manager.js contributor | ||
| ``` | ||
|
|
||
| ### Dry-Run Mode | ||
|
|
||
| Test the script without making any actual changes: | ||
|
|
||
| ```bash | ||
| # Using flag | ||
| node pr-quota-manager.js username --dry-run | ||
|
|
||
| # Using environment variable | ||
| DRY_RUN=true node pr-quota-manager.js username | ||
| ``` | ||
|
|
||
| In dry-run mode, the script will: | ||
| - Show exactly what actions it would take | ||
| - Not create/modify labels | ||
| - Not post comments | ||
| - Not make any API modifications | ||
| - Still fetch and analyze PRs for accurate simulation | ||
|
|
||
| ## Listing Open PRs by Author | ||
|
|
||
| Use the utility script to see all open PRs grouped by author: | ||
|
|
||
| ```bash | ||
| node list-open-prs-by-author.js [owner] [repo] | ||
| ``` | ||
|
|
||
| This is useful for: | ||
| - Identifying which users need quota processing | ||
| - Planning backfills of the quota system | ||
| - Seeing which PRs are already quota-blocked | ||
|
|
||
| **CSV output for scripting:** | ||
| ```bash | ||
| FORMAT=csv node list-open-prs-by-author.js > prs.csv | ||
| ``` | ||
|
|
||
| ## Output | ||
|
|
||
| The script will display: | ||
|
|
||
| 1. **History Audit**: Summary of merged PR count (up to 3 merged PRs for quota calculation) | ||
| 2. **Current Stats**: Merged count, calculated quota, and open PR count | ||
| 3. **Processing Actions**: Each PR being blocked/unblocked | ||
| 4. **Summary**: Total counts of blocked, unblocked, and unchanged PRs | ||
|
|
||
| ### Example Output | ||
|
|
||
| ``` | ||
| === Processing Quota for: @newuser === | ||
|
|
||
| 📜 History Audit: | ||
| No merged PRs found. | ||
|
|
||
| 📊 Current Stats: | ||
| User has 0 merged PRs. Current Quota: 1. Currently Open: 3. | ||
|
|
||
| 🔄 Processing Open PRs: | ||
|
|
||
| ℹ️ PR #123 unchanged (active) | ||
| ✅ Labeled PR #124 as blocked (Position: 2/3, Quota: 1) | ||
| ✅ Labeled PR #125 as blocked (Position: 3/3, Quota: 1) | ||
|
|
||
| ✅ Processing Complete for @newuser | ||
|
|
||
| 📋 Summary: | ||
| - Blocked: 2 PRs | ||
| - Unblocked: 0 PRs | ||
| - Unchanged: 1 PRs | ||
| ``` | ||
|
|
||
| ## Running Tests | ||
|
|
||
| To run the unit tests: | ||
|
|
||
| ```bash | ||
| cd .github/scripts | ||
| npm test | ||
| ``` | ||
|
|
||
| To run tests with coverage: | ||
|
|
||
| ```bash | ||
| npm test -- --coverage | ||
| ``` | ||
|
|
||
| ## Quota Rules | ||
|
|
||
| The script applies the following quota rules: | ||
|
|
||
| | Merged PRs | Quota | | ||
| |-----------|-------| | ||
| | 0 | 1 | | ||
| | 1 | 2 | | ||
| | 2 | 3 | | ||
| | 3+ | 10 | | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### "GITHUB_TOKEN environment variable is required" | ||
|
|
||
| Make sure you've set the `GITHUB_TOKEN` environment variable: | ||
| ```bash | ||
| export GITHUB_TOKEN="your_token_here" | ||
| ``` | ||
|
|
||
| ### "403 Forbidden" errors | ||
|
|
||
| Your GitHub token may not have the required permissions. Ensure it has: | ||
| - `repo` scope for private repositories | ||
| - `public_repo` scope for public repositories | ||
|
|
||
| ### "Cannot find module '@octokit/rest'" | ||
|
|
||
| Install the required dependency: | ||
| ```bash | ||
| cd .github/scripts | ||
| npm install @octokit/rest | ||
| ``` | ||
|
|
||
| ### API Rate Limiting | ||
|
|
||
| GitHub has rate limits for API requests: | ||
| - Authenticated requests: 5,000 requests per hour | ||
| - The script makes approximately 2-5 API calls per user | ||
|
|
||
| If you hit rate limits, wait for the limit to reset or use a different token. | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. **Fetches PRs** by the target author (all open PRs + up to 3 merged PRs for quota calculation) | ||
| 2. **Calculates quota** based on the number of merged PRs | ||
| 3. **Identifies open PRs** and sorts them by creation date (oldest first) | ||
| 4. **Applies labels** to PRs based on quota: | ||
| - PRs within quota: Remove `pr-quota-reached` label (if present) | ||
| - PRs exceeding quota: Add `pr-quota-reached` label | ||
| 5. **Posts comments** (only on state changes to avoid spam): | ||
| - Blocking comment when PR first gets blocked | ||
| - Unblocking comment when PR is moved to active queue | ||
|
|
||
| ## Integration with GitHub Actions | ||
|
|
||
| This script is automatically executed by the GitHub Actions workflow (`.github/workflows/pr-quota-manager.yml`) on: | ||
| - Pull request opened, closed, or reopened events | ||
| - Manual workflow dispatch | ||
|
|
||
| The workflow uses `actions/github-script` to run the script with the repository's built-in `GITHUB_TOKEN`. | ||
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,170 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| /** | ||
| * List Open PRs Grouped by Author | ||
| * | ||
| * This utility script lists all open PRs in a repository grouped by author. | ||
| * Useful for identifying which users need quota processing or backfilling. | ||
| * | ||
| * Usage: | ||
| * GITHUB_TOKEN=<token> node list-open-prs-by-author.js [owner] [repo] | ||
| */ | ||
|
|
||
| /** | ||
| * Fetch all open PRs grouped by author | ||
| * @param {object} octokit - GitHub API client | ||
| * @param {string} owner - Repository owner | ||
| * @param {string} repo - Repository name | ||
| * @returns {Promise<Map>} Map of author -> array of PRs | ||
| */ | ||
| async function fetchOpenPRsByAuthor(octokit, owner, repo) { | ||
| const prsByAuthor = new Map(); | ||
| let page = 1; | ||
| const perPage = 100; | ||
|
|
||
| console.log(`📥 Fetching open PRs from ${owner}/${repo}...`); | ||
|
|
||
| while (true) { | ||
| const { data } = await octokit.rest.pulls.list({ | ||
| owner, | ||
| repo, | ||
| state: 'open', | ||
| per_page: perPage, | ||
| page, | ||
| sort: 'created', | ||
| direction: 'asc' | ||
| }); | ||
|
|
||
| if (data.length === 0) break; | ||
|
|
||
| for (const pr of data) { | ||
| const author = pr.user.login; | ||
| if (!prsByAuthor.has(author)) { | ||
| prsByAuthor.set(author, []); | ||
| } | ||
| prsByAuthor.get(author).push({ | ||
| number: pr.number, | ||
| title: pr.title, | ||
| created_at: pr.created_at, | ||
| labels: pr.labels.map(l => l.name) | ||
| }); | ||
| } | ||
|
|
||
| if (data.length < perPage) break; | ||
| page++; | ||
| } | ||
|
|
||
| return prsByAuthor; | ||
| } | ||
|
|
||
| /** | ||
| * Display PRs grouped by author | ||
| * @param {Map} prsByAuthor - Map of author -> PRs | ||
| */ | ||
| function displayResults(prsByAuthor) { | ||
| // Sort by number of open PRs (descending) | ||
| const sortedAuthors = Array.from(prsByAuthor.entries()) | ||
| .sort((a, b) => b[1].length - a[1].length); | ||
|
|
||
| console.log(`\n📊 Found ${sortedAuthors.length} authors with open PRs\n`); | ||
| console.log('=' .repeat(80)); | ||
|
|
||
| for (const [author, prs] of sortedAuthors) { | ||
| const hasQuotaLabel = prs.some(pr => pr.labels.includes('pr-quota-reached')); | ||
| const quotaIndicator = hasQuotaLabel ? ' 🚫' : ''; | ||
|
|
||
| console.log(`\n👤 @${author} (${prs.length} open PR${prs.length > 1 ? 's' : ''})${quotaIndicator}`); | ||
|
|
||
| // Sort PRs by creation date (oldest first) | ||
| const sortedPRs = prs.sort((a, b) => | ||
| new Date(a.created_at) - new Date(b.created_at) | ||
| ); | ||
|
|
||
| for (const pr of sortedPRs) { | ||
| const date = new Date(pr.created_at).toISOString().split('T')[0]; | ||
| const quotaLabel = pr.labels.includes('pr-quota-reached') ? ' [QUOTA REACHED]' : ''; | ||
| console.log(` - PR #${pr.number}: ${pr.title.substring(0, 70)}${pr.title.length > 70 ? '...' : ''}`); | ||
| console.log(` Created: ${date}${quotaLabel}`); | ||
| } | ||
| } | ||
|
|
||
| console.log('\n' + '='.repeat(80)); | ||
| console.log(`\n📋 Summary:`); | ||
| console.log(` Total authors: ${sortedAuthors.length}`); | ||
| console.log(` Total open PRs: ${Array.from(prsByAuthor.values()).reduce((sum, prs) => sum + prs.length, 0)}`); | ||
|
|
||
| const authorsWithQuota = sortedAuthors.filter(([_, prs]) => | ||
| prs.some(pr => pr.labels.includes('pr-quota-reached')) | ||
| ).length; | ||
| if (authorsWithQuota > 0) { | ||
| console.log(` Authors with quota-blocked PRs: ${authorsWithQuota}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Display in CSV format for easy processing | ||
| * @param {Map} prsByAuthor - Map of author -> PRs | ||
| */ | ||
| function displayCSV(prsByAuthor) { | ||
| console.log('Author,PR Count,PR Numbers,Has Quota Label'); | ||
|
|
||
| for (const [author, prs] of prsByAuthor.entries()) { | ||
| const prNumbers = prs.map(pr => `#${pr.number}`).join(' '); | ||
| const hasQuotaLabel = prs.some(pr => pr.labels.includes('pr-quota-reached')); | ||
| console.log(`${author},${prs.length},"${prNumbers}",${hasQuotaLabel}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Main execution function | ||
| */ | ||
| async function main() { | ||
| const args = process.argv.slice(2); | ||
|
|
||
| const owner = args[0] || process.env.GITHUB_REPOSITORY?.split('/')[0] || 'jaegertracing'; | ||
| const repo = args[1] || process.env.GITHUB_REPOSITORY?.split('/')[1] || 'jaeger'; | ||
| const format = process.env.FORMAT || 'default'; // 'default' or 'csv' | ||
|
|
||
| if (!process.env.GITHUB_TOKEN) { | ||
| console.error('Error: GITHUB_TOKEN environment variable is required'); | ||
| console.error('Usage: GITHUB_TOKEN=<token> node list-open-prs-by-author.js [owner] [repo]'); | ||
| console.error('Optional: FORMAT=csv for CSV output'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // Import @octokit/rest dynamically | ||
| const { Octokit } = await import('@octokit/rest'); | ||
| const octokit = new Octokit({ | ||
| auth: process.env.GITHUB_TOKEN | ||
| }); | ||
|
|
||
| try { | ||
| const prsByAuthor = await fetchOpenPRsByAuthor(octokit, owner, repo); | ||
|
|
||
| if (format === 'csv') { | ||
| displayCSV(prsByAuthor); | ||
| } else { | ||
| displayResults(prsByAuthor); | ||
| } | ||
| } catch (error) { | ||
| console.error('Error:', error.message); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| // Export for testing | ||
| if (typeof module !== 'undefined' && module.exports) { | ||
| module.exports = { | ||
| fetchOpenPRsByAuthor, | ||
| displayResults, | ||
| displayCSV | ||
| }; | ||
| } | ||
|
|
||
| // Run main function if executed directly | ||
| if (require.main === module) { | ||
| main().catch(error => { | ||
| console.error('Fatal error:', error); | ||
| process.exit(1); | ||
| }); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.