diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml new file mode 100644 index 0000000..15e69a4 --- /dev/null +++ b/.github/workflows/example.yml @@ -0,0 +1,36 @@ +# This is an example workflow showing how to use the Issue & PR Automation Suite +# Copy this file to your repository's .github/workflows/ directory and customize as needed + +name: Issue & PR Automation Suite + +on: + issues: + types: [opened, closed, reopened, labeled, unlabeled, milestoned, demilestoned, edited] + pull_request: + types: [opened, closed, reopened, ready_for_review, converted_to_draft, synchronize, edited] + pull_request_target: + types: [opened, synchronize] + +permissions: + issues: write + pull-requests: write + repository-projects: write + contents: read + +env: + PROJECT_NAME: "Portfolio Devmt" + +jobs: + automation: + name: Run Automation Suite + runs-on: ubuntu-latest + steps: + - name: Issue & PR Automation + uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + project-name: ${{ env.PROJECT_NAME }} + enable-project-automation: "true" + enable-label-sync: "true" + enable-zap-labeling: "true" + zap-labels: "Meta,Stylistic,javascript,meta:seq,ZAP!" diff --git a/.gitignore b/.gitignore index 5d947ca..78c91d2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,15 @@ bin-release/ # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` # should NOT be excluded as they contain compiler settings and other important # information for Eclipse / Flash Builder. + +# macOS +.DS_Store + +# IDE +.vscode/ +.idea/ + +# Temporary files +*.tmp +*.log +.env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..88aaf03 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] - 2025-01-XX + +### Added +- Initial release of Issue & PR Automation Suite +- Project board automation for GitHub Projects V2 + - Automatically add new issues to project backlog + - Update issue status based on PR lifecycle (In Progress, In Review) +- Label and milestone synchronization between issues and PRs + - Bidirectional label sync + - Smart milestone sync +- ZAP security scan issue auto-labeling +- Configurable inputs for all features +- Comprehensive documentation with examples + +### Features +- Support for composite GitHub Action +- Integration with GitHub Projects V2 GraphQL API +- Automatic issue linking detection (multiple patterns supported) +- Modular feature enablement (can enable/disable individual features) + +[Unreleased]: https://github.com/SillyLittleTech/AutomationSuite/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/SillyLittleTech/AutomationSuite/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b32ec76 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# Contributing to Issue & PR Automation Suite + +Thank you for your interest in contributing! We welcome contributions from the community. + +## How to Contribute + +### Reporting Bugs + +If you find a bug, please open an issue with: +- A clear, descriptive title +- Steps to reproduce the issue +- Expected behavior +- Actual behavior +- Your workflow configuration (redact sensitive information) +- Relevant logs from the workflow run + +### Suggesting Enhancements + +We welcome feature requests! Please open an issue with: +- A clear description of the feature +- Use cases that would benefit from this feature +- Any implementation ideas you might have + +### Pull Requests + +1. Fork the repository +2. Create a new branch for your feature (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Update documentation if needed +5. Test your changes thoroughly +6. Commit your changes (`git commit -m 'Add amazing feature'`) +7. Push to your branch (`git push origin feature/amazing-feature`) +8. Open a Pull Request + +#### PR Guidelines + +- Follow the existing code style +- Update the README.md if you're adding new features or changing behavior +- Add your changes to CHANGELOG.md under [Unreleased] +- Ensure the action.yml syntax is valid +- Test your changes in a real repository before submitting +- Keep PRs focused on a single feature or fix + +### Development Setup + +1. Clone the repository +2. Make changes to `action.yml` +3. Test locally by: + - Creating a test repository + - Adding a workflow that uses your local action: + ```yaml + uses: your-username/AutomationSuite@your-branch + ``` + - Trigger the workflow and verify behavior + +### Testing + +Before submitting a PR, please test: +- All three features (project automation, label sync, ZAP labeling) independently +- Feature combinations (e.g., project + label sync) +- Edge cases (empty labels, missing projects, invalid issue references) +- Different event types (issue opened, PR created, labels changed, etc.) + +### Documentation + +- Update README.md for user-facing changes +- Add examples for new features +- Update input parameter documentation +- Keep the troubleshooting section up to date + +## Code of Conduct + +- Be respectful and inclusive +- Provide constructive feedback +- Focus on what's best for the community +- Show empathy towards other community members + +## Questions? + +Feel free to open an issue for any questions about contributing! + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/README.md b/README.md index 7b98d0d..c12461d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,362 @@ -# AutomationSuite -GitHub automation suite for syncing labels and milestones from Issues to PRs +# Issue & PR Automation Suite 🤖 + +[![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Issue%20%26%20PR%20Automation-blue?logo=github)](https://github.com/marketplace/actions/issue-pr-automation-suite) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A comprehensive GitHub Action that automates issue and pull request workflows including: + +- 📋 **Project Board Integration** - Automatically add issues to GitHub Projects V2 and update status based on PR lifecycle +- đŸˇī¸ **Label & Milestone Sync** - Keep labels and milestones synchronized between linked issues and PRs +- 🔒 **ZAP Security Scan Auto-labeling** - Automatically label ZAP security scan report issues + +## Quick Start + +```yaml +- uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} +``` + +## Version Pinning + +We recommend pinning to a specific version for stability: + +- `@v1` - Latest v1.x.x release (recommended for most users) +- `@v1.0.0` - Specific release version (maximum stability) +- `@main` - Latest development version (not recommended for production) + +## Features + +### đŸŽ¯ Project Board Automation + +- **New Issues**: Automatically adds opened issues to your GitHub Projects V2 board in "Backlog" status +- **PR Status Updates**: Updates linked issue status based on PR lifecycle: + - PR opened/converted to draft → Issue moves to "In Progress" + - PR ready for review → Issue moves to "In Review" + - PR merged → Issue moves to "In Review" (customizable) + - PR closed without merge → Issue status unchanged + +### 🔄 Label & Milestone Synchronization + +- **Bidirectional Label Sync**: Labels are automatically synchronized between issues and their linked PRs + - When issue labels change → Updates all linked open PRs + - When PR opens → Inherits labels from all linked issues +- **Smart Milestone Sync**: Milestones are intelligently synchronized + - Issue has milestone, PR doesn't → Apply issue milestone to PR + - PR has milestone, issue doesn't → Apply PR milestone to issue + +### 🔐 ZAP Security Scan Auto-labeling + +- Automatically detects issues with "ZAP Scan Baseline Report" in the title +- Applies configurable labels (default: `Meta`, `Stylistic`, `javascript`, `meta:seq`, `ZAP!`) +- Triggers on issue creation and edits + +## Usage + +### Basic Setup + +Create a workflow file (e.g., `.github/workflows/automation.yml`) in your repository: + +```yaml +name: Issue & PR Automation + +on: + issues: + types: [opened, closed, reopened, labeled, unlabeled, milestoned, demilestoned, edited] + pull_request: + types: [opened, closed, reopened, ready_for_review, converted_to_draft, synchronize, edited] + pull_request_target: + types: [opened, synchronize] + +permissions: + issues: write + pull-requests: write + repository-projects: write + contents: read + +jobs: + automation: + runs-on: ubuntu-latest + steps: + - name: Run Automation Suite + uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + project-name: "My Project" +``` + +### Advanced Configuration + +Customize the behavior with input parameters: + +```yaml +- name: Run Automation Suite + uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + project-name: "Portfolio Devmt" + enable-project-automation: "true" + enable-label-sync: "true" + enable-zap-labeling: "true" + zap-labels: "security,automated,zap-scan,needs-review" +``` + +### Selective Features + +Enable only specific features: + +#### Project Board Automation Only + +```yaml +- name: Project Board Automation + uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + project-name: "Sprint Board" + enable-project-automation: "true" + enable-label-sync: "false" + enable-zap-labeling: "false" +``` + +#### Label & Milestone Sync Only + +```yaml +- name: Label & Milestone Sync + uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + enable-project-automation: "false" + enable-label-sync: "true" + enable-zap-labeling: "false" +``` + +#### ZAP Auto-labeling Only + +```yaml +- name: ZAP Security Issue Labeling + uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + enable-project-automation: "false" + enable-label-sync: "false" + enable-zap-labeling: "true" + zap-labels: "security,zap,needs-triage" +``` + +## Input Parameters + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `github-token` | GitHub token for API access | Yes | `${{ github.token }}` | +| `project-name` | Name of GitHub Projects V2 project | No | `Portfolio Devmt` | +| `enable-project-automation` | Enable project board automation | No | `true` | +| `enable-label-sync` | Enable label/milestone sync | No | `true` | +| `enable-zap-labeling` | Enable ZAP issue auto-labeling | No | `true` | +| `zap-labels` | Comma-separated labels for ZAP issues | No | `Meta,Stylistic,javascript,meta:seq,ZAP!` | + +## How It Works + +### Issue Linking + +The action detects linked issues in PRs using multiple patterns: + +- Direct reference: `#123` +- Closing keywords: `closes #123`, `fixes #456`, `resolves #789` +- Full URLs: `https://github.com/owner/repo/issues/123` + +### Project Board Status Flow + +```mermaid +graph LR + A[Issue Created] -->|Opened| B[Backlog] + B -->|PR Opened| C[In Progress] + C -->|Ready for Review| D[In Review] + D -->|PR Merged| D + C -->|Converted to Draft| C +``` + +### Label Synchronization Flow + +- **Issue labeled** → All linked open PRs receive the same labels +- **Issue unlabeled** → Labels removed from all linked open PRs +- **PR opened** → Inherits all labels from linked issues +- **Bidirectional** → Changes propagate in both directions + +## Examples + +### Example 1: Full Automation with Custom Project + +```yaml +name: Complete Automation Suite + +on: + issues: + types: [opened, closed, reopened, labeled, unlabeled, milestoned, demilestoned, edited] + pull_request: + types: [opened, closed, reopened, ready_for_review, converted_to_draft, synchronize, edited] + +permissions: + issues: write + pull-requests: write + repository-projects: write + contents: read + +jobs: + automation: + runs-on: ubuntu-latest + steps: + - name: Run Full Automation + uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + project-name: "Development Board" + zap-labels: "security,scan,automated,needs-review" +``` + +### Example 2: Multiple Jobs for Different Features + +```yaml +name: Modular Automation + +on: + issues: + types: [opened, closed, reopened, labeled, unlabeled, milestoned, demilestoned, edited] + pull_request: + types: [opened, closed, reopened, ready_for_review, converted_to_draft, synchronize, edited] + +permissions: + issues: write + pull-requests: write + repository-projects: write + contents: read + +jobs: + project-management: + name: Project Board Sync + runs-on: ubuntu-latest + if: github.event_name == 'issues' || github.event_name == 'pull_request' + steps: + - uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + project-name: "Sprint Planning" + enable-label-sync: "false" + enable-zap-labeling: "false" + + label-sync: + name: Label & Milestone Sync + runs-on: ubuntu-latest + steps: + - uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + enable-project-automation: "false" + enable-zap-labeling: "false" + + security-labeling: + name: Security Scan Labeling + runs-on: ubuntu-latest + if: github.event_name == 'issues' + steps: + - uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + enable-project-automation: "false" + enable-label-sync: "false" + zap-labels: "security,vulnerability,zap-baseline" +``` + +### Example 3: Security-Focused Setup + +```yaml +name: Security Automation + +on: + issues: + types: [opened, edited] + +permissions: + issues: write + +jobs: + security-triage: + runs-on: ubuntu-latest + steps: + - name: Auto-label Security Scans + uses: SillyLittleTech/AutomationSuite@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + enable-project-automation: "false" + enable-label-sync: "false" + enable-zap-labeling: "true" + zap-labels: "security,automated-scan,needs-triage,zap-report" +``` + +## Prerequisites + +### GitHub Projects V2 + +For project board automation to work: + +1. Create a GitHub Projects V2 board (user or organization level) +2. Ensure the board has a "Status" field with the following options: + - `Backlog` (for new issues) + - `In Progress` (for issues with open PRs) + - `In Review` (for issues with PRs ready for review) +3. Set the `project-name` input to match your project's exact name + +### Permissions + +The workflow requires the following permissions: + +```yaml +permissions: + issues: write # For updating issue labels and milestones + pull-requests: write # For updating PR labels and milestones + repository-projects: write # For managing project board items + contents: read # For reading repository content +``` + +## Troubleshooting + +### Project not found + +If you see "Project not found" errors: +- Verify the project name matches exactly (case-sensitive) +- Ensure the project is at the user/organization level (not repository-level) +- Check that the token has access to the project + +### Labels not syncing + +- Ensure both issues and PRs have write permissions +- Check that the PR body or title contains valid issue references +- Verify labels exist in the repository + +### ZAP issues not being labeled + +- Confirm the issue title contains "ZAP Scan Baseline Report" (case-insensitive) +- Verify labels specified in `zap-labels` exist in your repository +- Check workflow permissions include `issues: write` + +## Contributing + +Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Support + +If you encounter any issues or have questions: +- Open an issue in this repository +- Check existing issues for solutions +- Review the workflow run logs for detailed error messages + +## Acknowledgments + +Built with â¤ī¸ using GitHub Actions and the GitHub GraphQL API. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..cd191ee --- /dev/null +++ b/action.yml @@ -0,0 +1,587 @@ +name: 'Issue & PR Automation Suite' +description: 'Comprehensive automation for GitHub Issues and Pull Requests including project board management, label/milestone synchronization, and ZAP security scan auto-labeling' +author: 'SillyLittleTech' + +branding: + icon: 'git-pull-request' + color: 'blue' + +inputs: + github-token: + description: 'GitHub token for API access' + required: true + default: ${{ github.token }} + project-name: + description: 'Name of the GitHub Projects V2 project to manage' + required: false + default: 'Portfolio Devmt' + enable-project-automation: + description: 'Enable project board automation' + required: false + default: 'true' + enable-label-sync: + description: 'Enable label and milestone synchronization between issues and PRs' + required: false + default: 'true' + enable-zap-labeling: + description: 'Enable automatic labeling of ZAP security scan issues' + required: false + default: 'true' + zap-labels: + description: 'Comma-separated list of labels to apply to ZAP scan issues' + required: false + default: 'Meta,Stylistic,javascript,meta:seq,ZAP!' + +runs: + using: 'composite' + steps: + - name: Project Board Automation + if: ${{ inputs.enable-project-automation == 'true' && ((github.event_name == 'issues' && github.event.action == 'opened') || github.event_name == 'pull_request') }} + uses: actions/github-script@v7 + with: + github-token: ${{ inputs.github-token }} + script: | + const projectName = '${{ inputs.project-name }}'; + + if (context.eventName === 'issues' && context.payload.action === 'opened') { + const issue = context.payload.issue; + const issueId = issue.node_id; + const issueNumber = issue.number; + + // Query to find the project and add the issue + const findProjectQuery = ` + query($owner: String!) { + user(login: $owner) { + projectsV2(first: 20) { + nodes { + id + title + fields(first: 20) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + } + `; + + try { + // Find the project + const projectResult = await github.graphql(findProjectQuery, { + owner: context.repo.owner + }); + + const project = projectResult.user.projectsV2.nodes.find(p => + p.title === projectName + ); + + if (!project) { + console.log(`❌ Project "${projectName}" not found`); + console.log('Available projects:', projectResult.user.projectsV2.nodes.map(p => p.title)); + return; + } + + console.log(`✅ Found project: ${project.title} (${project.id})`); + + // Add issue to project + const addItemMutation = ` + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: { + projectId: $projectId + contentId: $contentId + }) { + item { + id + } + } + } + `; + + const addResult = await github.graphql(addItemMutation, { + projectId: project.id, + contentId: issueId + }); + + const itemId = addResult.addProjectV2ItemById.item.id; + console.log(`✅ Added issue #${issueNumber} to project (item ID: ${itemId})`); + + // Find the Status field and Backlog option + const statusField = project.fields.nodes.find(f => + f.name === 'Status' && f.options + ); + + if (!statusField) { + console.log('âš ī¸ Status field not found, item added without specific status'); + return; + } + + const backlogOption = statusField.options.find(o => + o.name === 'Backlog' + ); + + if (!backlogOption) { + console.log('âš ī¸ Backlog status not found'); + console.log('Available statuses:', statusField.options.map(o => o.name)); + return; + } + + // Set item status to Backlog + const updateStatusMutation = ` + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: $value + }) { + projectV2Item { + id + } + } + } + `; + + await github.graphql(updateStatusMutation, { + projectId: project.id, + itemId: itemId, + fieldId: statusField.id, + value: { singleSelectOptionId: backlogOption.id } + }); + + console.log(`✅ Set issue #${issueNumber} status to "Backlog"`); + + } catch (error) { + console.error('❌ Error managing project:', error.message); + if (error.errors) { + console.error('GraphQL errors:', JSON.stringify(error.errors, null, 2)); + } + } + } else if (context.eventName === 'pull_request') { + const pr = context.payload.pull_request; + const prNumber = pr.number; + const action = context.payload.action; + + // Extract issue numbers from PR body and title + const prText = `${pr.title} ${pr.body || ''}`; + const issuePattern = /#(\d+)|(?:close[ds]?|fix(?:e[ds]?)?|resolve[ds]?)\s+#(\d+)|(?:https?:\/\/)?(?:www\.)?github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/gi; + const issueNumbers = new Set(); + let match; + + while ((match = issuePattern.exec(prText)) !== null) { + const issueNum = match[1] || match[2] || match[3]; + if (issueNum) { + issueNumbers.add(parseInt(issueNum)); + } + } + + if (issueNumbers.size === 0) { + console.log('â„šī¸ No linked issues found in PR'); + return; + } + + console.log(`🔗 Found ${issueNumbers.size} linked issue(s):`, Array.from(issueNumbers)); + + // Determine target status based on PR state and action + let targetStatus = null; + + if (action === 'opened' || action === 'converted_to_draft') { + targetStatus = 'In Progress'; + } else if (action === 'ready_for_review') { + targetStatus = 'In Review'; + } else if (action === 'closed' && pr.merged) { + targetStatus = 'In Review'; // Set to In Review when merged (not Done, per requirements) + } else if (action === 'closed' && !pr.merged) { + console.log('â„šī¸ PR closed without merging - not updating issue status'); + return; + } + + if (!targetStatus) { + console.log(`â„šī¸ No status update needed for action: ${action}`); + return; + } + + console.log(`📊 Target status: "${targetStatus}"`); + + // Query to find the project and get issue details + const findProjectQuery = ` + query($owner: String!) { + user(login: $owner) { + projectsV2(first: 20) { + nodes { + id + title + fields(first: 20) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + items(first: 100) { + nodes { + id + content { + ... on Issue { + number + } + } + } + } + } + } + } + } + `; + + try { + // Find the project + const projectResult = await github.graphql(findProjectQuery, { + owner: context.repo.owner + }); + + const project = projectResult.user.projectsV2.nodes.find(p => + p.title === projectName + ); + + if (!project) { + console.log(`❌ Project "${projectName}" not found`); + return; + } + + console.log(`✅ Found project: ${project.title}`); + + // Find the Status field and target option + const statusField = project.fields.nodes.find(f => + f.name === 'Status' && f.options + ); + + if (!statusField) { + console.log('❌ Status field not found in project'); + return; + } + + const targetOption = statusField.options.find(o => + o.name === targetStatus + ); + + if (!targetOption) { + console.log(`❌ Status "${targetStatus}" not found in project`); + console.log('Available statuses:', statusField.options.map(o => o.name)); + return; + } + + // Update status for each linked issue + const updateStatusMutation = ` + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: $value + }) { + projectV2Item { + id + } + } + } + `; + + for (const issueNumber of issueNumbers) { + // Find the project item for this issue + const projectItem = project.items.nodes.find(item => + item.content && item.content.number === issueNumber + ); + + if (!projectItem) { + console.log(`âš ī¸ Issue #${issueNumber} not found in project, skipping`); + continue; + } + + await github.graphql(updateStatusMutation, { + projectId: project.id, + itemId: projectItem.id, + fieldId: statusField.id, + value: { singleSelectOptionId: targetOption.id } + }); + + console.log(`✅ Updated issue #${issueNumber} status to "${targetStatus}"`); + } + + } catch (error) { + console.error('❌ Error updating issue status:', error.message); + if (error.errors) { + console.error('GraphQL errors:', JSON.stringify(error.errors, null, 2)); + } + } + } + + - name: Label & Milestone Synchronization + if: ${{ inputs.enable-label-sync == 'true' }} + uses: actions/github-script@v7 + with: + github-token: ${{ inputs.github-token }} + script: | + const { owner, repo } = context.repo; + const eventName = context.eventName; + const action = context.payload.action; + + console.log(`Event: ${eventName}, Action: ${action}`); + + // Helper function to extract linked issue numbers from text + function extractIssueNumbers(text) { + const issuePattern = /#(\d+)|(?:close[ds]?|fix(?:e[ds]?)?|resolve[ds]?)\s+#(\d+)|(?:https?:\/\/)?(?:www\.)?github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/gi; + const issueNumbers = new Set(); + let match; + + while ((match = issuePattern.exec(text)) !== null) { + const issueNum = match[1] || match[2] || match[3]; + if (issueNum) { + issueNumbers.add(parseInt(issueNum)); + } + } + + return Array.from(issueNumbers); + } + + // Helper function to find PRs that link to a specific issue + async function findLinkedPRs(issueNumber) { + const { data: prs } = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + }); + + const linkedPRs = []; + + for (const pr of prs) { + const prText = `${pr.title} ${pr.body || ''}`; + const linkedIssues = extractIssueNumbers(prText); + + if (linkedIssues.includes(issueNumber)) { + linkedPRs.push(pr.number); + } + } + + return linkedPRs; + } + + // ISSUE EVENTS + if (eventName === 'issues') { + const issue = context.payload.issue; + const issueNumber = issue.number; + + if (action === 'opened') { + console.log(`✨ New issue #${issueNumber} opened`); + } + + if (action === 'labeled' || action === 'unlabeled') { + console.log(`đŸˇī¸ Issue #${issueNumber} labels changed`); + + // Find linked PRs and sync labels + const linkedPRs = await findLinkedPRs(issueNumber); + console.log(`Found ${linkedPRs.length} linked PRs:`, linkedPRs); + + if (linkedPRs.length > 0) { + const issueLabels = issue.labels.map(label => + typeof label === 'object' ? label.name : label + ).filter(Boolean); + + console.log('📋 Issue labels:', issueLabels); + + for (const prNumber of linkedPRs) { + try { + await github.rest.issues.setLabels({ + owner, + repo, + issue_number: prNumber, + labels: issueLabels, + }); + + console.log(`✅ Synced labels to PR #${prNumber}`); + } catch (error) { + console.log(`❌ Failed to sync labels to PR #${prNumber}:`, error.message); + } + } + } + } + + if (action === 'milestoned' || action === 'demilestoned') { + console.log(`📅 Issue #${issueNumber} milestone changed`); + + // Find linked PRs and sync milestone + const linkedPRs = await findLinkedPRs(issueNumber); + + for (const prNumber of linkedPRs) { + try { + await github.rest.issues.update({ + owner, + repo, + issue_number: prNumber, + milestone: issue.milestone ? issue.milestone.number : null, + }); + + console.log(`✅ Synced milestone to PR #${prNumber}`); + } catch (error) { + console.log(`❌ Failed to sync milestone to PR #${prNumber}:`, error.message); + } + } + } + } + + // PULL REQUEST EVENTS + if (eventName === 'pull_request' || eventName === 'pull_request_target') { + const pr = context.payload.pull_request; + const prNumber = pr.number; + const prText = `${pr.title} ${pr.body || ''}`; + const linkedIssues = extractIssueNumbers(prText); + + console.log(`🔀 PR #${prNumber} event: ${action}`); + console.log(`Found ${linkedIssues.length} linked issues:`, linkedIssues); + + if (action === 'opened') { + // Sync labels from linked issues to PR + if (linkedIssues.length > 0) { + const allLabels = new Set(); + + for (const issueNumber of linkedIssues) { + try { + const { data: issue } = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + }); + + issue.labels.forEach(label => { + if (typeof label === 'object' && label.name) { + allLabels.add(label.name); + } + }); + + } catch (error) { + console.log(`❌ Could not fetch issue #${issueNumber}:`, error.message); + } + } + + if (allLabels.size > 0) { + try { + await github.rest.issues.setLabels({ + owner, + repo, + issue_number: prNumber, + labels: Array.from(allLabels), + }); + + console.log(`✅ Applied ${allLabels.size} labels to PR #${prNumber}`); + } catch (error) { + console.log(`❌ Failed to apply labels to PR #${prNumber}:`, error.message); + } + } + } + } + + // Sync milestones + if (linkedIssues.length > 0) { + for (const issueNumber of linkedIssues) { + try { + const { data: issue } = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + }); + + // Sync milestone from issue to PR if PR doesn't have one + if (issue.milestone && !pr.milestone) { + await github.rest.issues.update({ + owner, + repo, + issue_number: prNumber, + milestone: issue.milestone.number, + }); + + console.log(`✅ Applied milestone "${issue.milestone.title}" from issue #${issueNumber} to PR #${prNumber}`); + } + // Sync milestone from PR to issue if issue doesn't have one + else if (!issue.milestone && pr.milestone) { + await github.rest.issues.update({ + owner, + repo, + issue_number: issueNumber, + milestone: pr.milestone.number, + }); + + console.log(`✅ Applied milestone "${pr.milestone.title}" from PR #${prNumber} to issue #${issueNumber}`); + } + + } catch (error) { + console.log(`❌ Could not sync milestone for issue #${issueNumber}:`, error.message); + } + } + } + } + + console.log('🎉 Label & milestone sync completed') + + - name: Auto-label ZAP Security Scan Issues + if: ${{ inputs.enable-zap-labeling == 'true' && github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'edited') }} + uses: actions/github-script@v7 + with: + github-token: ${{ inputs.github-token }} + script: | + const { owner, repo } = context.repo; + const issue = context.payload.issue; + const issueNumber = issue.number; + const issueTitle = issue.title || ''; + + console.log(`Checking issue #${issueNumber}: "${issueTitle}"`); + + // Configurable pattern for ZAP Scan Baseline Report (case-insensitive) + const zapPattern = 'ZAP Scan Baseline Report'; + if (issueTitle.toLowerCase().includes(zapPattern.toLowerCase())) { + console.log('✅ Issue title matches ZAP Scan Baseline Report pattern'); + + // Parse labels from input + const labelsToApply = '${{ inputs.zap-labels }}'.split(',').map(l => l.trim()).filter(Boolean); + + // Get current labels on the issue + const currentLabels = issue.labels.map(label => + typeof label === 'object' ? label.name : label + ).filter(Boolean); + + console.log('Current labels:', currentLabels); + + // Combine current labels with new labels (avoiding duplicates) + const allLabels = [...new Set([...currentLabels, ...labelsToApply])]; + + console.log('Applying labels:', allLabels); + + try { + await github.rest.issues.setLabels({ + owner, + repo, + issue_number: issueNumber, + labels: allLabels, + }); + + console.log(`✅ Successfully applied labels to issue #${issueNumber}`); + } catch (error) { + console.error(`❌ Failed to apply labels to issue #${issueNumber}:`, error.message); + } + } else { + console.log('â„šī¸ Issue title does not match ZAP Scan Baseline Report pattern, skipping'); + } + + console.log('✅ ZAP issue labeling completed')