|
| 1 | +name: Monthly Issue Report |
| 2 | + |
| 3 | +on: |
| 4 | + schedule: |
| 5 | + # Run on the 1st of every month at 9:00 AM UTC |
| 6 | + - cron: "0 9 1 * *" |
| 7 | + workflow_dispatch: |
| 8 | + inputs: |
| 9 | + dry_run: |
| 10 | + description: 'Dry run (print report without updating issue)' |
| 11 | + type: boolean |
| 12 | + default: false |
| 13 | + |
| 14 | +jobs: |
| 15 | + generate-report: |
| 16 | + runs-on: ubuntu-latest |
| 17 | + permissions: |
| 18 | + issues: write |
| 19 | + steps: |
| 20 | + - uses: actions/github-script@v7 |
| 21 | + with: |
| 22 | + github-token: ${{ secrets.UI5_WEBCOMP_BOT_GH_TOKEN }} |
| 23 | + script: | |
| 24 | + const REPORT_ISSUE_NUMBER = 13080; // Monthly Issues Report tracking issue |
| 25 | + const isDryRun = context.eventName === 'workflow_dispatch' && ${{ inputs.dry_run || false }}; |
| 26 | +
|
| 27 | + // All TOPIC labels to track (team labels) |
| 28 | + const topics = [ |
| 29 | + 'TOPIC B', |
| 30 | + 'TOPIC Core', |
| 31 | + 'TOPIC P', |
| 32 | + 'TOPIC RD', |
| 33 | + 'TOPIC RL', |
| 34 | + 'TOPIC SKR', |
| 35 | + 'TOPIC TBL' |
| 36 | + ]; |
| 37 | +
|
| 38 | + // Helper to check if issue is a bug (by label OR issue type) |
| 39 | + const isBug = (issue) => { |
| 40 | + const labels = issue.labels.map(l => l.name.toLowerCase()); |
| 41 | + const typeName = issue.type?.name?.toLowerCase() || ''; |
| 42 | + return labels.includes('bug') || typeName === 'bug'; |
| 43 | + }; |
| 44 | +
|
| 45 | + // Helper to check if issue is a feature request (by label OR issue type) |
| 46 | + const isFeatureRequest = (issue) => { |
| 47 | + const labels = issue.labels.map(l => l.name.toLowerCase()); |
| 48 | + const typeName = issue.type?.name?.toLowerCase() || ''; |
| 49 | + return labels.includes('feature request') || labels.includes('enhancement') || typeName === 'feature'; |
| 50 | + }; |
| 51 | +
|
| 52 | + // Calculate date range for last month |
| 53 | + const now = new Date(); |
| 54 | + const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); |
| 55 | + const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59); |
| 56 | + const monthName = lastMonth.toLocaleString('en-US', { month: 'long', year: 'numeric' }); |
| 57 | +
|
| 58 | + const sinceDate = lastMonth.toISOString(); |
| 59 | + const untilDate = lastMonthEnd.toISOString(); |
| 60 | +
|
| 61 | + console.log(`Generating report for ${monthName}`); |
| 62 | + console.log(`Date range: ${sinceDate} to ${untilDate}`); |
| 63 | +
|
| 64 | + const results = []; |
| 65 | +
|
| 66 | + for (const topic of topics) { |
| 67 | + // Get issues with this label using raw API to include 'type' field |
| 68 | + const allIssues = await github.paginate('GET /repos/{owner}/{repo}/issues', { |
| 69 | + owner: context.repo.owner, |
| 70 | + repo: context.repo.repo, |
| 71 | + labels: topic, |
| 72 | + state: 'all', |
| 73 | + since: sinceDate, |
| 74 | + per_page: 100 |
| 75 | + }); |
| 76 | +
|
| 77 | + // Filter to issues created within the date range (exclude PRs) |
| 78 | + const opened = allIssues.filter(issue => { |
| 79 | + const created = new Date(issue.created_at); |
| 80 | + return created >= lastMonth && created <= lastMonthEnd && !issue.pull_request; |
| 81 | + }); |
| 82 | +
|
| 83 | + // Get closed issues - filter those closed in the period |
| 84 | + const closed = allIssues.filter(issue => { |
| 85 | + if (!issue.closed_at || issue.pull_request) return false; |
| 86 | + const closedDate = new Date(issue.closed_at); |
| 87 | + return closedDate >= lastMonth && closedDate <= lastMonthEnd; |
| 88 | + }); |
| 89 | +
|
| 90 | + // Split by type |
| 91 | + const openedBugs = opened.filter(isBug); |
| 92 | + const openedFRs = opened.filter(isFeatureRequest); |
| 93 | + const openedOther = opened.filter(i => !isBug(i) && !isFeatureRequest(i)); |
| 94 | +
|
| 95 | + const closedBugs = closed.filter(isBug); |
| 96 | + const closedFRs = closed.filter(isFeatureRequest); |
| 97 | + const closedOther = closed.filter(i => !isBug(i) && !isFeatureRequest(i)); |
| 98 | +
|
| 99 | + results.push({ |
| 100 | + topic: topic, |
| 101 | + bugs: { opened: openedBugs.length, closed: closedBugs.length }, |
| 102 | + frs: { opened: openedFRs.length, closed: closedFRs.length }, |
| 103 | + other: { opened: openedOther.length, closed: closedOther.length } |
| 104 | + }); |
| 105 | + } |
| 106 | +
|
| 107 | + // Build markdown report |
| 108 | + let report = `## ${monthName} Issue Report\n\n`; |
| 109 | +
|
| 110 | + // Bugs table |
| 111 | + report += `### Bugs\n\n`; |
| 112 | + report += `| Topic | Opened | Closed |\n`; |
| 113 | + report += `|-------|--------|--------|\n`; |
| 114 | +
|
| 115 | + let totalBugsOpened = 0, totalBugsClosed = 0; |
| 116 | + for (const r of results) { |
| 117 | + report += `| ${r.topic} | ${r.bugs.opened} | ${r.bugs.closed} |\n`; |
| 118 | + totalBugsOpened += r.bugs.opened; |
| 119 | + totalBugsClosed += r.bugs.closed; |
| 120 | + } |
| 121 | + report += `| **Total** | **${totalBugsOpened}** | **${totalBugsClosed}** |\n\n`; |
| 122 | +
|
| 123 | + // Feature Requests table |
| 124 | + report += `### Feature Requests\n\n`; |
| 125 | + report += `| Topic | Opened | Closed |\n`; |
| 126 | + report += `|-------|--------|--------|\n`; |
| 127 | +
|
| 128 | + let totalFRsOpened = 0, totalFRsClosed = 0; |
| 129 | + for (const r of results) { |
| 130 | + report += `| ${r.topic} | ${r.frs.opened} | ${r.frs.closed} |\n`; |
| 131 | + totalFRsOpened += r.frs.opened; |
| 132 | + totalFRsClosed += r.frs.closed; |
| 133 | + } |
| 134 | + report += `| **Total** | **${totalFRsOpened}** | **${totalFRsClosed}** |\n\n`; |
| 135 | +
|
| 136 | + // Summary |
| 137 | + report += `### Summary\n\n`; |
| 138 | + report += `| Type | Opened | Closed |\n`; |
| 139 | + report += `|------|--------|--------|\n`; |
| 140 | + report += `| Bugs | ${totalBugsOpened} | ${totalBugsClosed} |\n`; |
| 141 | + report += `| Feature Requests | ${totalFRsOpened} | ${totalFRsClosed} |\n`; |
| 142 | + const grandOpened = totalBugsOpened + totalFRsOpened; |
| 143 | + const grandClosed = totalBugsClosed + totalFRsClosed; |
| 144 | + report += `| **Total** | **${grandOpened}** | **${grandClosed}** |\n`; |
| 145 | +
|
| 146 | + report += `\n_Report generated: ${now.toISOString()}_\n`; |
| 147 | +
|
| 148 | + console.log('\n' + report); |
| 149 | +
|
| 150 | + if (isDryRun) { |
| 151 | + console.log('\nDry run - not updating issue'); |
| 152 | + return; |
| 153 | + } |
| 154 | +
|
| 155 | + if (REPORT_ISSUE_NUMBER === 0) { |
| 156 | + console.log('\nREPORT_ISSUE_NUMBER not set - skipping issue update'); |
| 157 | + console.log('Create a tracking issue and set REPORT_ISSUE_NUMBER to its number'); |
| 158 | + return; |
| 159 | + } |
| 160 | +
|
| 161 | + // Add report as a comment on the tracking issue |
| 162 | + await github.rest.issues.createComment({ |
| 163 | + owner: context.repo.owner, |
| 164 | + repo: context.repo.repo, |
| 165 | + issue_number: REPORT_ISSUE_NUMBER, |
| 166 | + body: report |
| 167 | + }); |
| 168 | +
|
| 169 | + console.log(`\nAdded comment to issue #${REPORT_ISSUE_NUMBER} with new report`); |
0 commit comments