diff --git a/.github/AUTOLABELER_FEATURES.md b/.github/AUTOLABELER_FEATURES.md new file mode 100644 index 000000000..3fdfb2237 --- /dev/null +++ b/.github/AUTOLABELER_FEATURES.md @@ -0,0 +1,458 @@ +# ๐Ÿค– Autolabeler Features + +This document describes the automated features of our PR labeling system. + +## ๐ŸŽฏ All Features + +### 1. **Label Cleanup** โœจ + +Automatically removes outdated or conflicting labels when PR is updated. + +**Example:** + +- PR initially labeled with `feature` and `bugfix` +- User updates PR and only checks `bugfix` +- Bot removes `feature` label automatically + +**Benefits:** + +- Prevents label confusion +- Ensures accurate changelog categorization +- Maintains label consistency + +--- + +### 2. **Commit Message Analysis** ๐Ÿ“ + +Automatically detects change types from conventional commit messages. + +**Supported Formats:** + +- `fix:` or `fix(scope):` โ†’ adds `bugfix` label +- `feat:` or `feat(scope):` โ†’ adds `feature` label +- `refactor:` or `refactor(scope):` โ†’ adds `refactor` label +- `BREAKING CHANGE:` or `feat!:` โ†’ adds `breaking change` label + +**Example:** + +``` +feat(docker): add support for custom networks + +This adds support for user-defined networks +``` + +โ†’ Automatically labeled with `feature` + +**Benefits:** + +- Works even if checkboxes are forgotten +- Supports standard Git conventions +- Combines with template checkboxes + +--- + +### 3. **Size Labels** ๐Ÿ“ + +Automatically adds size labels based on total lines changed. + +**Size Categories:** + +- `size: XS` โ†’ 1-10 lines (๐ŸŸข Green) +- `size: S` โ†’ 11-50 lines (๐ŸŸข Yellow-Green) +- `size: M` โ†’ 51-200 lines (๐ŸŸก Yellow) +- `size: L` โ†’ 201-500 lines (๐ŸŸ  Orange) +- `size: XL` โ†’ 500+ lines (๐Ÿ”ด Red) + +**Benefits:** + +- Helps reviewers prioritize +- Quick visual indication of PR complexity +- Automatic and consistent + +**Note:** Labels need to be created in repository first. See [SETUP_NEW_LABELS.md](SETUP_NEW_LABELS.md) + +--- + +### 4. **First-Time Contributor Welcome** ๐ŸŽ‰ + +Special welcome message for contributors making their first PR. + +**Example:** + +```markdown +## ๐ŸŽ‰ Welcome to the Community! + +Thank you @username for your first contribution! ๐Ÿ™Œ + +A maintainer will review your PR soon. Here are some helpful resources: + +- Contributing Guide +- Code of Conduct +``` + +**Benefits:** + +- Better onboarding experience +- Increases contributor retention +- Builds community + +--- + +### 5. **Documentation Check** ๐Ÿ“š + +Warns when code changes don't include documentation updates. + +**Triggers Warning When:** + +- Code files modified (`.sh`, `.func`, `.js`, `.go`, etc.) +- No documentation files modified (`.md`, `README`, etc.) +- Not a bugfix or refactor (features should have docs!) + +**Example Warning:** + +```markdown +๐Ÿ“š **Documentation update recommended**: Consider updating documentation +for these code changes. +``` + +**Benefits:** + +- Improves documentation coverage +- Reminds contributors about docs +- Better project maintainability + +--- + +### 6. **Related Issues Detection** ๐Ÿ”— + +Automatically finds and links related open issues. + +**How it works:** + +- Extracts script names from changed files +- Searches open issues for matching names +- Links up to 5 most relevant issues + +**Example:** + +```markdown +## ๐Ÿ”— Related Issues + +This PR may be related to the following open issues: + +- #123: Docker script fails on Ubuntu 22.04 +- #456: Add network configuration options +``` + +**Benefits:** + +- Better context for reviewers +- Automatic cross-referencing +- Helps track issue resolution + +--- + +### 7. **Validation Warnings** โš ๏ธ + +Provides helpful warnings when PR information is incomplete or inconsistent. + +**Checks:** + +- โœ… Change type checkbox is selected for script updates +- โœ… Website checkbox matches file changes +- โœ… Required labels are present +- โœ… Documentation updated for new features + +**Example Warning:** + +```markdown +## โš ๏ธ Validation Warnings + +โš ๏ธ **Missing change type**: Please check one of the change type checkboxes +(Bug fix, New feature, Refactoring, or Breaking change) in the PR description. +``` + +--- + +### 8. **Changelog Preview** ๐Ÿ“ + +Shows contributors exactly how their PR will appear in the changelog. + +**Example Preview:** + +```markdown +## ๐Ÿ“ Changelog Preview + +This PR will appear in the changelog as: + +### ๐Ÿš€ Updated Scripts + +- #### ๐Ÿž Bug Fixes + - Fix docker script issue @username ([#123](url)) +``` + +**Benefits:** + +- Contributors see immediate feedback +- Reduces changelog errors +- Improves PR quality + +--- + +## ๐Ÿท๏ธ Label Priority System + +When multiple change types are checked, only the highest priority label is applied: + +1. ๐Ÿž **Bugfix** (highest priority) +2. ๐Ÿ”ง **Refactor** +3. โœจ **Feature** +4. ๐Ÿ’ฅ **Breaking Change** (lowest priority) + +**Rationale:** + +- Bug fixes are most important for users +- Prevents PRs from appearing in multiple subcategories +- Maintains clean changelog structure + +**Note:** This applies to both template checkboxes AND conventional commit messages! + +--- + +## ๐Ÿ“Š Changelog Categories + +The bot automatically determines which changelog category your PR belongs to: + +| Files Changed | Category | Note | +| ------------------------------------ | ------------------------------- | ------------------ | +| `ct/`, `vm/`, `install/`, `turnkey/` | ๐Ÿš€ Updated Scripts | Standard scripts | +| `tools/` | ๐Ÿ› ๏ธ Updated Tools | Utility tools | +| `misc/` | ๐Ÿ”ง Core Updates | Core functionality | +| `frontend/` (not JSON) | ๐ŸŒ Website | Frontend changes | +| `frontend/public/json/` | ๐ŸŒ Website > Script Information | Metadata updates | +| Documentation files | ๐Ÿงฐ Maintenance | Docs, README, etc. | + +**Multi-Category PRs:** +PRs that change files in multiple categories will appear in each relevant category. + +--- + +## ๐Ÿ”„ Bot Behavior + +### When PR is Opened/Edited: + +1. โœ… Analyzes changed files +2. โœ… Parses PR template checkboxes +3. โœ… Analyzes commit messages (conventional commits) +4. โœ… Calculates size label +5. โœ… Checks if first-time contributor +6. โœ… Applies appropriate labels +7. โœ… Removes conflicting labels +8. โœ… Checks for validation issues +9. โœ… Finds related issues +10. โœ… Posts/updates comment with all info + +### Comment Structure: + +A single bot comment contains (in order): + +1. ๐ŸŽ‰ Welcome message (if first-time contributor) +2. โš ๏ธ Validation warnings (if any) +3. ๐Ÿ“ Changelog preview (always) +4. ๐Ÿ”— Related issues (if found) +5. โ„น๏ธ Footer with PR size + +### Comment Updates: + +- Bot updates its own comment when PR is edited (no spam) +- Only posts new comment if none exists +- Welcome message is preserved across updates + +--- + +## ๐Ÿ“ Complete Example + +Here's what a first-time contributor might see: + +```markdown +## ๐ŸŽ‰ Welcome to the Community! + +Thank you @newuser for your first contribution! ๐Ÿ™Œ + +A maintainer will review your PR soon. Here are some helpful resources: + +- Contributing Guide +- Code of Conduct + +--- + +## โš ๏ธ Validation Warnings + +๐Ÿ“š **Documentation update recommended**: Consider updating documentation +for these code changes. + +--- + +## ๐Ÿ“ Changelog Preview + +This PR will appear in the changelog as: + +### ๐Ÿš€ Updated Scripts + +- #### โœจ New Features + - Add Docker network support @newuser ([#789](url)) + +--- + +## ๐Ÿ”— Related Issues + +This PR may be related to the following open issues: + +- #456: Docker networking not working +- #123: Request: Custom network support + +--- + +_This is an automated message from the PR labeler bot. PR size: size: M_ +``` + +--- + +## ๐Ÿ› ๏ธ For Maintainers + +### Debugging Label Issues + +If labels aren't being applied correctly: + +1. Check PR template checkboxes are properly formatted +2. Verify commit messages use conventional format +3. Look for bot comment explaining label decisions +4. Check GitHub Actions logs for errors +5. Verify all required labels exist in repository + +### Modifying Label Behavior + +**Config files:** + +- `.github/autolabeler-config.json` - File path โ†’ label mappings +- `.github/changelog-pr-config.json` - Changelog categories +- `.github/workflows/autolabeler.yml` - Core logic + +**Priority order** can be changed in `autolabeler.yml`: + +```javascript +const priorityOrder = ["bugfix", "refactor", "feature", "breaking change"]; +``` + +**Size thresholds** can be adjusted: + +```javascript +if (totalChanges <= 10) labelsToAdd.add("size: XS"); +else if (totalChanges <= 50) labelsToAdd.add("size: S"); +// etc. +``` + +### Creating Required Labels + +Run these commands to create all size labels: + +```bash +gh label create "size: XS" --description "1-10 lines" --color "00ff00" +gh label create "size: S" --description "11-50 lines" --color "7fff00" +gh label create "size: M" --description "51-200 lines" --color "ffff00" +gh label create "size: L" --description "201-500 lines" --color "ff8c00" +gh label create "size: XL" --description "500+ lines" --color "ff0000" +``` + +See [SETUP_NEW_LABELS.md](SETUP_NEW_LABELS.md) for full setup guide. + +--- + +## ๐Ÿงช Testing Scenarios + +### Test 1: Conventional Commits + +```bash +# Commit with conventional format +git commit -m "feat: add new feature" +# Expected: feature label, even without checkbox +``` + +### Test 2: Size Labels + +```bash +# Small PR (< 50 lines) +# Expected: size: S label +``` + +### Test 3: First-Time Contributor + +```bash +# Create PR from new contributor account +# Expected: Welcome message in bot comment +``` + +### Test 4: Documentation Check + +```bash +# Change .sh file, no .md changes +# Expected: Warning about missing docs +``` + +### Test 5: Related Issues + +```bash +# PR changes "docker.sh" +# Expected: Links to issues mentioning "docker" +``` + +--- + +## ๐ŸŽ‰ Benefits Summary + +โœ… **Automation**: Less manual work for maintainers +โœ… **Consistency**: All PRs follow same standard +โœ… **Quality**: Multiple validation checks +โœ… **Transparency**: Contributors see what happens +โœ… **Community**: Better onboarding for new contributors +โœ… **Context**: Automatic issue linking +โœ… **Clean Changelogs**: Priority system prevents duplication + +--- + +## ๐Ÿ› Troubleshooting + +**Labels not applied?** + +- Check if PR template checkboxes are properly formatted +- Try using conventional commit messages +- Ensure files changed match expected patterns +- Look for validation warnings in bot comment + +**Size label missing?** + +- Labels need to be created first (see setup guide) +- Check if label was removed manually +- Verify workflow has permissions to add labels + +**Wrong category?** + +- Verify file paths (tools/ vs ct/ vs misc/) +- Check if multiple categories apply (intended behavior) +- Review `.github/changelog-pr-config.json` + +**Bot comment not appearing?** + +- Check GitHub Actions logs +- Verify bot has comment permissions +- May take a few seconds after PR creation +- Check if PR is from a fork (requires pull_request_target) + +**Related issues not found?** + +- Script name must match issue title/body +- Only searches open issues +- Limited to top 5 results + +--- + +**Questions?** Check the workflow logs or ask in the repository discussions. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 99aafb954..f2036e8c4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,9 +11,8 @@ body: - ๐Ÿ”Ž If you encounter `[ERROR] in line 23: exit code *: while executing command "$@" > /dev/null 2>&1`, rerun the script with verbose mode before submitting the issue. - ๐Ÿ“œ **Read the script:** Familiarize yourself with the script's content and its purpose. This will help you understand the issue better and provide more relevant information - Thank you for taking the time to report an issue! Please provide as much detail as possible to help us address the problem efficiently. + Thank you for taking the time to report an issue! Please provide as much detail as possible to help us address the problem efficiently. - - type: input id: guidelines attributes: @@ -34,8 +33,7 @@ body: id: script_command attributes: label: ๐Ÿ“‚ What was the exact command used to execute the script? - placeholder: "e.g., bash -c \"$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/zigbee2mqtt.sh)\" or \"update\"" - validations: + placeholder: 'e.g., bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/zigbee2mqtt.sh)" or "update"' required: true - type: checkboxes @@ -56,14 +54,15 @@ body: attributes: label: ๐Ÿ–ฅ๏ธ Which Linux distribution are you using? options: - - + - - Alpine - Debian 11 - Debian 12 - Debian 13 - Ubuntu 22.04 - Ubuntu 24.04 - - Ubuntu 24.10 + - Ubuntu 25.04 + - Other validations: required: true diff --git a/.github/SETUP_NEW_LABELS.md b/.github/SETUP_NEW_LABELS.md new file mode 100644 index 000000000..ad700b617 --- /dev/null +++ b/.github/SETUP_NEW_LABELS.md @@ -0,0 +1,79 @@ +# ๐Ÿท๏ธ Setup New Labels + +The autolabeler now uses additional size labels. These need to be created in your repository. + +## Required Labels + +### Size Labels (New) + +These labels are automatically added based on the number of lines changed: + +| Label | Lines Changed | Color | Description | +| ---------- | ------------- | --------- | ----------------------------- | +| `size: XS` | 1-10 | `#00ff00` | Extra Small - Minimal changes | +| `size: S` | 11-50 | `#7fff00` | Small - Minor changes | +| `size: M` | 51-200 | `#ffff00` | Medium - Moderate changes | +| `size: L` | 201-500 | `#ff8c00` | Large - Significant changes | +| `size: XL` | 500+ | `#ff0000` | Extra Large - Major changes | + +## Quick Setup Commands + +Run these commands in your terminal to create all labels at once: + +```bash +# Create size labels +gh label create "size: XS" --description "Extra Small - 1-10 lines changed" --color "00ff00" +gh label create "size: S" --description "Small - 11-50 lines changed" --color "7fff00" +gh label create "size: M" --description "Medium - 51-200 lines changed" --color "ffff00" +gh label create "size: L" --description "Large - 201-500 lines changed" --color "ff8c00" +gh label create "size: XL" --description "Extra Large - 500+ lines changed" --color "ff0000" +``` + +## Manual Setup (GitHub UI) + +1. Go to: `https://github.com/community-scripts/ProxmoxVE/labels` +2. Click "New label" for each label +3. Fill in: + - **Name**: Use exact names from table above + - **Description**: Copy from table + - **Color**: Use hex codes from table (without #) + +## Verification + +After creating labels, test by: + +1. Creating a small PR (< 10 lines) โ†’ should get `size: XS` +2. Check that bot comment includes size in footer + +## Optional: Label Sync + +For automated label management, consider using [github-label-sync](https://github.com/Financial-Times/github-label-sync): + +```json +{ + "size: XS": { + "color": "00ff00", + "description": "Extra Small - 1-10 lines changed" + }, + "size: S": { + "color": "7fff00", + "description": "Small - 11-50 lines changed" + }, + "size: M": { + "color": "ffff00", + "description": "Medium - 51-200 lines changed" + }, + "size: L": { + "color": "ff8c00", + "description": "Large - 201-500 lines changed" + }, + "size: XL": { + "color": "ff0000", + "description": "Extra Large - 500+ lines changed" + } +} +``` + +--- + +**Note**: The autolabeler will work even if labels don't exist yet, but they won't be visible on PRs until created. diff --git a/.github/autolabeler-config.json b/.github/autolabeler-config.json index 342a1e38f..abf620e45 100644 --- a/.github/autolabeler-config.json +++ b/.github/autolabeler-config.json @@ -101,20 +101,11 @@ "excludeGlobs": [] } ], - "addon": [ + "tools": [ { "fileStatus": null, "includeGlobs": [ - "tools/addon/**" - ], - "excludeGlobs": [] - } - ], - "pve-tool": [ - { - "fileStatus": null, - "includeGlobs": [ - "tools/pve/**" + "tools/**" ], "excludeGlobs": [] } diff --git a/.github/changelog-pr-config.json b/.github/changelog-pr-config.json index e556703d0..47bb7ecf7 100644 --- a/.github/changelog-pr-config.json +++ b/.github/changelog-pr-config.json @@ -10,6 +10,10 @@ "labels": [ "update script" ], + "excludeLabels": [ + "tools", + "core" + ], "subCategories": [ { "title": "๐Ÿž Bug Fixes", @@ -18,6 +22,13 @@ ], "notes": [] }, + { + "title": "๐Ÿ”ง Refactor", + "labels": [ + "refactor" + ], + "notes": [] + }, { "title": "โœจ New Features", "labels": [ @@ -31,6 +42,61 @@ "breaking change" ], "notes": [] + } + ] + }, + { + "title": "๐Ÿ› ๏ธ Updated Tools", + "labels": [ + "update script", + "tools" + ], + "requireAllLabels": true, + "subCategories": [ + { + "title": "๏ฟฝ Bug Fixes", + "labels": [ + "bugfix" + ], + "notes": [] + }, + { + "title": "๏ฟฝ๐Ÿ”ง Refactor", + "labels": [ + "refactor" + ], + "notes": [] + }, + { + "title": "โœจ New Features", + "labels": [ + "feature" + ], + "notes": [] + }, + { + "title": "๐Ÿ’ฅ Breaking Changes", + "labels": [ + "breaking change" + ], + "notes": [] + } + ] + }, + { + "title": "๐Ÿ”ง Core Updates", + "labels": [ + "update script", + "core" + ], + "requireAllLabels": true, + "subCategories": [ + { + "title": "๐Ÿž Bug Fixes", + "labels": [ + "bugfix" + ], + "notes": [] }, { "title": "๐Ÿ”ง Refactor", @@ -38,6 +104,63 @@ "refactor" ], "notes": [] + }, + { + "title": "โœจ New Features", + "labels": [ + "feature" + ], + "notes": [] + }, + { + "title": "๐Ÿ’ฅ Breaking Changes", + "labels": [ + "breaking change" + ], + "notes": [] + } + ] + }, + { + "title": "๐ŸŒ Website", + "labels": [ + "website" + ], + "subCategories": [ + { + "title": "๏ฟฝ Bug Fixes", + "labels": [ + "bugfix" + ], + "notes": [] + }, + { + "title": "๏ฟฝ Refactor", + "labels": [ + "refactor" + ], + "notes": [] + }, + { + "title": "โœจ New Features", + "labels": [ + "feature" + ], + "notes": [] + }, + { + "title": "๏ฟฝ Breaking Changes", + "labels": [ + "breaking change" + ], + "notes": [] + }, + { + "title": "๏ฟฝ Script Information", + "labels": [ + "json" + ], + "notes": [] } ] }, @@ -54,6 +177,13 @@ ], "notes": [] }, + { + "title": "๐Ÿ”ง Refactor", + "labels": [ + "refactor" + ], + "notes": [] + }, { "title": "โœจ New Features", "labels": [ @@ -69,7 +199,7 @@ "notes": [] }, { - "title": "๐Ÿ“ก API", + "title": "๏ฟฝ API", "labels": [ "api" ], @@ -95,49 +225,6 @@ "maintenance" ], "notes": [] - }, - { - "title": "๐Ÿ”ง Refactor", - "labels": [ - "refactor" - ], - "notes": [] - } - ] - }, - { - "title": "๐ŸŒ Website", - "labels": [ - "website" - ], - "subCategories": [ - { - "title": "๐Ÿž Bug Fixes", - "labels": [ - "bugfix" - ], - "notes": [] - }, - { - "title": "โœจ New Features", - "labels": [ - "feature" - ], - "notes": [] - }, - { - "title": "๐Ÿ’ฅ Breaking Changes", - "labels": [ - "breaking change" - ], - "notes": [] - }, - { - "title": "๐Ÿ“ Script Information", - "labels": [ - "json" - ], - "notes": [] } ] }, diff --git a/.github/workflows/autolabeler.yml b/.github/workflows/autolabeler.yml index 03472a33f..b8deca807 100644 --- a/.github/workflows/autolabeler.yml +++ b/.github/workflows/autolabeler.yml @@ -35,6 +35,7 @@ jobs: const prNumber = context.payload.pull_request.number; const prBody = context.payload.pull_request.body || ""; + const prAuthor = context.payload.pull_request.user.login; let labelsToAdd = new Set(); @@ -45,6 +46,24 @@ jobs: }); const prFiles = prListFilesResponse.data; + // Get commits for commit message analysis + const { data: commits } = await github.rest.pulls.listCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + // Check if this is a first-time contributor + const { data: prsByAuthor } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + per_page: 5 + }); + const isFirstTimeContributor = !prsByAuthor.some(pr => + pr.user.login === prAuthor && pr.number !== prNumber && pr.merged_at + ); + for (const [label, rules] of Object.entries(autolabelerConfig)) { const shouldAddLabel = prFiles.some((prFile) => { return rules.some((rule) => { @@ -57,49 +76,316 @@ jobs: if (shouldAddLabel) { labelsToAdd.add(label); - if (label === "update script") { + if (label === "update script" || label === "new script") { for (const prFile of prFiles) { const filename = prFile.filename; if (filename.startsWith("vm/")) labelsToAdd.add("vm"); - if (filename.startsWith("tools/addon/")) labelsToAdd.add("addon"); - if (filename.startsWith("tools/pve/")) labelsToAdd.add("pve-tool"); + if (filename.startsWith("tools/")) labelsToAdd.add("tools"); + if (filename.startsWith("misc/")) labelsToAdd.add("core"); } } } } - if (labelsToAdd.size < 2) { - const templateLabelMappings = { - "๐Ÿž **Bug fix**": "bugfix", - "โœจ **New feature**": "feature", - "๐Ÿ’ฅ **Breaking change**": "breaking change", - "๐Ÿ†• **New script**": "new script", - "๐ŸŒ **Website update**": "website", // handled special - "๐Ÿ”ง **Refactoring / Code Cleanup**": "refactor", - "๐Ÿ“ **Documentation update**": "documentation" // mapped to maintenance - }; + // Feature 2: Analyze commit messages for conventional commits + const commitMessages = commits.map(c => c.commit.message).join('\n'); + const conventionalCommitLabels = []; - for (const [checkbox, label] of Object.entries(templateLabelMappings)) { - const escapedCheckbox = checkbox.replace(/([.*+?^=!:${}()|[\]\/\\])/g, "\\$1"); - const regex = new RegExp(`- \\[(x|X)\\]\\s*${escapedCheckbox}`, "i"); + if (/^fix(\(.*?\))?:/im.test(commitMessages)) { + conventionalCommitLabels.push("bugfix"); + } + if (/^feat(\(.*?\))?:/im.test(commitMessages)) { + conventionalCommitLabels.push("feature"); + } + if (/^refactor(\(.*?\))?:/im.test(commitMessages)) { + conventionalCommitLabels.push("refactor"); + } + if (/BREAKING[:\s]CHANGE/i.test(commitMessages) || /^[a-z]+(\(.*?\))?!:/im.test(commitMessages)) { + conventionalCommitLabels.push("breaking change"); + } - if (regex.test(prBody)) { - if (label === "website") { - const hasJson = prFiles.some((f) => f.filename.startsWith("frontend/public/json/")); - const hasUpdateScript = labelsToAdd.has("update script"); - const hasContentLabel = ["bugfix", "feature", "refactor"].some((l) => labelsToAdd.has(l)); + // Parse PR template for content type labels (bugfix, feature, refactor, breaking change) + const templateLabelMappings = { + "๐Ÿž **Bug fix**": "bugfix", + "โœจ **New feature**": "feature", + "๐Ÿ’ฅ **Breaking change**": "breaking change", + "๐Ÿ†• **New script**": "new script", + "๐ŸŒ **Website update**": "website", + "๐Ÿ”ง **Refactoring / Code Cleanup**": "refactor", + "๐Ÿ“ **Documentation update**": "documentation" + }; - if (!(hasUpdateScript && hasContentLabel)) { - labelsToAdd.add(hasJson ? "json" : "website"); + const contentLabels = ["bugfix", "feature", "refactor", "breaking change"]; + const checkedContentLabels = []; + + for (const [checkbox, label] of Object.entries(templateLabelMappings)) { + const escapedCheckbox = checkbox.replace(/([.*+?^=!:${}()|[\]\/\\])/g, "\\$1"); + const regex = new RegExp(`- \\[(x|X)\\]\\s*${escapedCheckbox}`, "i"); + + if (regex.test(prBody)) { + if (label === "website") { + const hasJson = prFiles.some((f) => f.filename.startsWith("frontend/public/json/")); + labelsToAdd.add(hasJson ? "json" : "website"); + } else if (label === "documentation") { + labelsToAdd.add("maintenance"); + } else if (label === "new script") { + labelsToAdd.add("new script"); + } else if (contentLabels.includes(label)) { + checkedContentLabels.push(label); + } else { + labelsToAdd.add(label); + } + } + } + + // Priority system: Combine template checkboxes and conventional commits + // Merge both sources + const allCheckedLabels = [...new Set([...checkedContentLabels, ...conventionalCommitLabels])]; + + // Priority: bugfix > refactor > feature > breaking change + if (allCheckedLabels.length > 0) { + const priorityOrder = ["bugfix", "refactor", "feature", "breaking change"]; + const highestPriorityLabel = priorityOrder.find(label => allCheckedLabels.includes(label)); + if (highestPriorityLabel) { + labelsToAdd.add(highestPriorityLabel); + } + } + + // Feature 3: Add size labels based on changes + const totalChanges = prFiles.reduce((sum, file) => sum + file.additions + file.deletions, 0); + if (totalChanges <= 10) { + labelsToAdd.add("size: XS"); + } else if (totalChanges <= 50) { + labelsToAdd.add("size: S"); + } else if (totalChanges <= 200) { + labelsToAdd.add("size: M"); + } else if (totalChanges <= 500) { + labelsToAdd.add("size: L"); + } else { + labelsToAdd.add("size: XL"); + } + + // Get current PR labels + const { data: currentPR } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + const currentLabels = currentPR.labels.map(label => label.name.toLowerCase()); + + // Label Cleanup: Remove outdated content labels and size labels + const allContentLabels = ["bugfix", "feature", "refactor", "breaking change"]; + const allSizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"]; + const labelsToRemove = [ + ...allContentLabels.filter(label => currentLabels.includes(label) && !labelsToAdd.has(label)), + ...allSizeLabels.filter(label => currentLabels.includes(label.toLowerCase()) && !labelsToAdd.has(label)) + ]; + + for (const label of labelsToRemove) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: label, + }); + } catch (error) { + // Label might not exist, ignore + } + } + + // Validation: Check for missing information + const warnings = []; + const hasUpdateOrNewScript = labelsToAdd.has("update script") || labelsToAdd.has("new script"); + const hasContentLabel = allContentLabels.some(label => labelsToAdd.has(label)); + + if (hasUpdateOrNewScript && !hasContentLabel && !labelsToAdd.has("new script")) { + warnings.push("โš ๏ธ **Missing change type**: Please check one of the change type checkboxes (Bug fix, New feature, Refactoring, or Breaking change) in the PR description."); + } + + if (labelsToAdd.has("website") && !labelsToAdd.has("json") && prFiles.some(f => f.filename.includes("frontend/"))) { + const hasCheckbox = /- \[(x|X)\]\s*๐ŸŒ \*\*Website update\*\*/i.test(prBody); + if (!hasCheckbox) { + warnings.push("โš ๏ธ **Missing checkbox**: Please check the '๐ŸŒ Website update' checkbox in the PR description."); + } + } + + // Feature 5: Documentation check + const codeFiles = prFiles.filter(f => /\.(sh|func|go|js|ts|tsx|jsx)$/.test(f.filename)); + const docFiles = prFiles.filter(f => /\.(md|txt)$/i.test(f.filename) || f.filename.includes('README')); + + if (codeFiles.length > 0 && docFiles.length === 0 && !labelsToAdd.has("refactor") && !labelsToAdd.has("bugfix")) { + warnings.push("๐Ÿ“š **Documentation update recommended**: Consider updating documentation for these code changes."); + } + + // Feature 6: Related issues detection + let relatedIssuesText = ""; + const scriptFiles = prFiles.filter(f => /\.(sh|func)$/.test(f.filename)); + if (scriptFiles.length > 0) { + const scriptNames = scriptFiles.map(f => { + const match = f.filename.match(/([^\/]+)\.(sh|func)$/); + return match ? match[1] : null; + }).filter(Boolean); + + if (scriptNames.length > 0) { + try { + const { data: openIssues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100 + }); + + const related = []; + for (const scriptName of scriptNames) { + const matchingIssues = openIssues.filter(issue => + !issue.pull_request && + (issue.title.toLowerCase().includes(scriptName.toLowerCase()) || + (issue.body || '').toLowerCase().includes(scriptName.toLowerCase())) + ); + related.push(...matchingIssues); + } + + const uniqueRelated = [...new Map(related.map(i => [i.number, i])).values()]; + if (uniqueRelated.length > 0) { + relatedIssuesText = "\n\n## ๐Ÿ”— Related Issues\n\n"; + relatedIssuesText += "This PR may be related to the following open issues:\n"; + uniqueRelated.slice(0, 5).forEach(issue => { + relatedIssuesText += `- #${issue.number}: ${issue.title}\n`; + }); + if (uniqueRelated.length > 5) { + relatedIssuesText += `- _...and ${uniqueRelated.length - 5} more_\n`; } - } else if (label === "documentation") { - labelsToAdd.add("maintenance"); - } else { - labelsToAdd.add(label); } + } catch (error) { + console.error("Error fetching related issues:", error); } } } + + // Generate Changelog Preview + let changelogPreview = "## ๐Ÿ“ Changelog Preview\n\n"; + changelogPreview += "This PR will appear in the changelog as:\n\n"; + + const categories = []; + + if (labelsToAdd.has("new script")) { + changelogPreview += "### ๐Ÿ†• New Scripts\n"; + changelogPreview += `- ${currentPR.title} @${currentPR.user.login} ([#${prNumber}](${currentPR.html_url}))\n\n`; + } else { + if (labelsToAdd.has("update script")) { + if (labelsToAdd.has("tools")) { + categories.push({ name: "๐Ÿ› ๏ธ Updated Tools", label: labelsToAdd }); + } else if (labelsToAdd.has("core")) { + categories.push({ name: "๐Ÿ”ง Core Updates", label: labelsToAdd }); + } else { + categories.push({ name: "๐Ÿš€ Updated Scripts", label: labelsToAdd }); + } + } + + if (labelsToAdd.has("website") || labelsToAdd.has("json")) { + categories.push({ name: "๐ŸŒ Website", label: labelsToAdd }); + } + + if (labelsToAdd.has("maintenance")) { + categories.push({ name: "๐Ÿงฐ Maintenance", label: labelsToAdd }); + } + + for (const category of categories) { + changelogPreview += `### ${category.name}\n`; + + const subCategoryMap = { + "bugfix": " - #### ๐Ÿž Bug Fixes", + "refactor": " - #### ๐Ÿ”ง Refactor", + "feature": " - #### โœจ New Features", + "breaking change": " - #### ๐Ÿ’ฅ Breaking Changes" + }; + + const matchedSubCat = allContentLabels.find(label => labelsToAdd.has(label)); + if (matchedSubCat && subCategoryMap[matchedSubCat]) { + changelogPreview += `${subCategoryMap[matchedSubCat]}\n`; + } + + if (labelsToAdd.has("json")) { + changelogPreview += " - #### ๐Ÿ“ Script Information\n"; + } + + changelogPreview += ` - ${currentPR.title} @${currentPR.user.login} ([#${prNumber}](${currentPR.html_url}))\n\n`; + } + + if (categories.length === 0) { + changelogPreview += "_This PR may not appear in the changelog. Please ensure proper labels are set._\n\n"; + } + } + + // Post comment with warnings and changelog preview + if (warnings.length > 0 || changelogPreview || relatedIssuesText || isFirstTimeContributor) { + let commentBody = ""; + + // Feature 4: First-time contributor welcome + if (isFirstTimeContributor) { + commentBody += "## ๐ŸŽ‰ Welcome to the Community!\n\n"; + commentBody += `Thank you **@${prAuthor}** for your first contribution! ๐Ÿ™Œ\n\n`; + commentBody += "A maintainer will review your PR soon. Here are some helpful resources:\n"; + commentBody += "- [Contributing Guide](https://github.com/community-scripts/ProxmoxVE/blob/main/.github/CONTRIBUTOR_AND_GUIDES/CONTRIBUTING.md)\n"; + commentBody += "- [Code of Conduct](https://github.com/community-scripts/ProxmoxVE/blob/main/.github/CODE_OF_CONDUCT.md)\n\n"; + commentBody += "---\n\n"; + } + + if (warnings.length > 0) { + commentBody += "## โš ๏ธ Validation Warnings\n\n"; + commentBody += warnings.join("\n\n") + "\n\n---\n\n"; + } + + commentBody += changelogPreview; + + if (relatedIssuesText) { + commentBody += relatedIssuesText; + } + + commentBody += "\n\n---\n"; + commentBody += `_This is an automated message from the PR labeler bot. PR size: ${Array.from(labelsToAdd).find(l => l.startsWith('size:')) || 'unknown'}_`; + + // Check if we already posted a comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const botComment = comments.find(comment => + comment.user.login === "github-actions[bot]" && + (comment.body.includes("Changelog Preview") || comment.body.includes("Welcome to the Community")) + ); + + if (botComment) { + // Update existing comment (but keep welcome message if it was there) + let updatedBody = commentBody; + if (botComment.body.includes("Welcome to the Community") && !isFirstTimeContributor) { + // Preserve welcome message from original comment + const welcomeSection = botComment.body.match(/## ๐ŸŽ‰ Welcome to the Community![\s\S]*?---\n\n/); + if (welcomeSection) { + updatedBody = welcomeSection[0] + commentBody; + } + } + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: updatedBody, + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: commentBody, + }); + } + } + if (labelsToAdd.size === 0) { labelsToAdd.add("needs triage"); } diff --git a/.github/workflows/changelog-pr.yml b/.github/workflows/changelog-pr.yml index d21f47c3d..c15e8f5c6 100644 --- a/.github/workflows/changelog-pr.yml +++ b/.github/workflows/changelog-pr.yml @@ -67,29 +67,12 @@ jobs: const categorizedPRs = changelogConfig.map(obj => ({ ...obj, notes: [], - subCategories: obj.subCategories ?? ( - obj.labels.includes("update script") ? [ - { title: "๐Ÿž Bug Fixes", labels: ["bugfix"], notes: [] }, - { title: "โœจ New Features", labels: ["feature"], notes: [] }, - { title: "๐Ÿ’ฅ Breaking Changes", labels: ["breaking change"], notes: [] }, - { title: "๐Ÿ”ง Refactor", labels: ["refactor"], notes: [] }, - ] : - obj.labels.includes("maintenance") ? [ - { title: "๐Ÿž Bug Fixes", labels: ["bugfix"], notes: [] }, - { title: "โœจ New Features", labels: ["feature"], notes: [] }, - { title: "๐Ÿ’ฅ Breaking Changes", labels: ["breaking change"], notes: [] }, - { title: "๐Ÿ“ก API", labels: ["api"], notes: [] }, - { title: "Github", labels: ["github"], notes: [] }, - { title: "๐Ÿ“ Documentation", labels: ["maintenance"], notes: [] }, - { title: "๐Ÿ”ง Refactor", labels: ["refactor"], notes: [] } - ] : - obj.labels.includes("website") ? [ - { title: "๐Ÿž Bug Fixes", labels: ["bugfix"], notes: [] }, - { title: "โœจ New Features", labels: ["feature"], notes: [] }, - { title: "๐Ÿ’ฅ Breaking Changes", labels: ["breaking change"], notes: [] }, - { title: "Script Information", labels: ["json"], notes: [] } - ] : [] - ) + requireAllLabels: obj.requireAllLabels ?? false, + excludeLabels: obj.excludeLabels ?? [], + subCategories: obj.subCategories ? obj.subCategories.map(sub => ({ + ...sub, + notes: [] + })) : [] })); const latestDateInChangelog = new Date(process.env.LATEST_DATE); @@ -115,69 +98,92 @@ jobs: for (const pr of filteredPRs) { const prLabels = pr.labels.map(label => label.name.toLowerCase()); + let prNote; + if (pr.user.login.includes("push-app-to-main[bot]")) { - const scriptName = pr.title; - try { - const { data: relatedIssues } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: "ProxmoxVED", - state: "all", - labels: ["Started Migration To ProxmoxVE"] - }); + try { + const { data: relatedIssues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: "ProxmoxVED", + state: "all", + labels: ["Started Migration To ProxmoxVE"] + }); - const matchingIssue = relatedIssues.find(issue => - issue.title.toLowerCase().includes(scriptName.toLowerCase()) - ); - - if (matchingIssue) { - const issueAuthor = matchingIssue.user.login; - const issueAuthorUrl = `https://github.com/${issueAuthor}`; - prNote = `- ${pr.title} [@${issueAuthor}](${issueAuthorUrl}) ([#${pr.number}](${pr.html_url}))`; - } - else { - prNote = `- ${pr.title} ([#${pr.number}](${pr.html_url}))`; - } - } catch (error) { - console.error(`Error fetching related issues: ${error}`); - prNote = `- ${pr.title} ([#${pr.number}](${pr.html_url}))`; - } - }else{ - prNote = `- ${pr.title} [@${pr.user.login}](https://github.com/${pr.user.login}) ([#${pr.number}](${pr.html_url}))`; - } - - - if (prLabels.includes("new script")) { - const newScriptCategory = categorizedPRs.find(category => - category.title === "New Scripts" || category.labels.includes("new script")); - if (newScriptCategory) { - newScriptCategory.notes.push(prNote); - } - } else { - - let categorized = false; - const priorityCategories = categorizedPRs.slice(); - for (const category of priorityCategories) { - if (categorized) break; - if (category.labels.some(label => prLabels.includes(label))) { - if (category.subCategories && category.subCategories.length > 0) { - const subCategory = category.subCategories.find(sub => - sub.labels.some(label => prLabels.includes(label)) + const matchingIssue = relatedIssues.find(issue => + issue.title.toLowerCase().includes(scriptName.toLowerCase()) ); - if (subCategory) { - subCategory.notes.push(prNote); + if (matchingIssue) { + const issueAuthor = matchingIssue.user.login; + prNote = `- ${pr.title} @${issueAuthor} ([#${pr.number}](${pr.html_url}))`; } else { - category.notes.push(prNote); + prNote = `- ${pr.title} ([#${pr.number}](${pr.html_url}))`; } - } else { - category.notes.push(prNote); - } - categorized = true; - } + } catch (error) { + console.error(`Error fetching related issues: ${error}`); + prNote = `- ${pr.title} ([#${pr.number}](${pr.html_url}))`; } + } else { + prNote = `- ${pr.title} @${pr.user.login} ([#${pr.number}](${pr.html_url}))`; } + // Handle "new script" separately - always goes to New Scripts category + if (prLabels.includes("new script")) { + const newScriptCategory = categorizedPRs.find(category => + category.title === "๐Ÿ†• New Scripts" || category.labels.includes("new script") + ); + if (newScriptCategory) { + newScriptCategory.notes.push(prNote); + } + continue; + } + + // For other PRs, check all applicable categories (can appear in multiple) + for (const category of categorizedPRs) { + // Skip "new script" and unlabelled categories + if (category.labels.includes("new script") || category.labels.length === 0) { + continue; + } + + // Check if PR matches category requirements + let matchesCategory = false; + + if (category.requireAllLabels) { + // All labels must be present + matchesCategory = category.labels.every(label => prLabels.includes(label)); + } else { + // At least one label must be present + matchesCategory = category.labels.some(label => prLabels.includes(label)); + } + + // Check exclude labels + if (matchesCategory && category.excludeLabels.length > 0) { + const hasExcludedLabel = category.excludeLabels.some(label => prLabels.includes(label)); + if (hasExcludedLabel) { + matchesCategory = false; + } + } + + if (matchesCategory) { + // Try to find matching subcategory + if (category.subCategories && category.subCategories.length > 0) { + const subCategory = category.subCategories.find(sub => + sub.labels.some(label => prLabels.includes(label)) + ); + + if (subCategory) { + subCategory.notes.push(prNote); + } else { + // No matching subcategory, add to main category + category.notes.push(prNote); + } + } else { + // No subcategories, add to main category + category.notes.push(prNote); + } + } + } } return categorizedPRs;