@@ -59,89 +59,107 @@ jobs:
5959 }
6060 }
6161
62- // Get commit statistics using GitHub's recommended approach - optimized
62+ // Cache for branch activity to avoid repeated API calls
63+ let branchActivityCache = null;
64+
65+ // Get active branches for the entire year (cache this)
66+ async function getActiveBranchesForYear() {
67+ if (branchActivityCache) {
68+ console.log('*** USING CACHED BRANCH ACTIVITY ***');
69+ return branchActivityCache;
70+ }
71+
72+ console.log('*** BUILDING BRANCH ACTIVITY CACHE FOR FULL YEAR ***');
73+
74+ // Get all branches
75+ const branches = await github.paginate(github.rest.repos.listBranches, {
76+ owner: context.repo.owner,
77+ repo: context.repo.repo,
78+ per_page: 100,
79+ });
80+
81+ console.log(`*** FOUND ${branches.length} BRANCHES ***`);
82+
83+ // Get the date range for the entire year we're analyzing
84+ const now = new Date();
85+ const yearStart = new Date(now.getFullYear(), now.getMonth() - 12, 1);
86+ const yearEnd = new Date();
87+
88+ console.log(`*** CHECKING ACTIVITY FROM ${yearStart.toISOString()} TO ${yearEnd.toISOString()} ***`);
89+
90+ const activeBranches = [];
91+ let branchesChecked = 0;
92+
93+ for (const branch of branches) {
94+ try {
95+ branchesChecked++;
96+ if (branchesChecked % 20 === 0) {
97+ console.log(`*** Checked ${branchesChecked}/${branches.length} branches for yearly activity ***`);
98+ }
99+
100+ // Check if branch has ANY commits in the entire year
101+ const branchCommits = await github.rest.repos.listCommits({
102+ owner: context.repo.owner,
103+ repo: context.repo.repo,
104+ sha: branch.name,
105+ since: yearStart.toISOString(),
106+ until: yearEnd.toISOString(),
107+ per_page: 1,
108+ });
109+
110+ if (branchCommits.data.length > 0) {
111+ activeBranches.push(branch);
112+ console.log(`*** Branch ${branch.name} has commits in the year ***`);
113+ }
114+
115+ } catch (error) {
116+ console.log(`*** Error checking ${branch.name}: ${error.message} ***`);
117+ }
118+
119+ await new Promise(resolve => setTimeout(resolve, 50));
120+ }
121+
122+ console.log(`*** FOUND ${activeBranches.length} ACTIVE BRANCHES FOR THE YEAR ***`);
123+ branchActivityCache = activeBranches;
124+ return activeBranches;
125+ }
126+
127+ // Get commit statistics using cached active branches
63128 async function getMonthCommitStats(since, until) {
64- console.log(`=== GETTING COMMITS FROM ACTIVE BRANCHES ${since} TO ${until} ===`);
129+ console.log(`=== GETTING COMMITS FROM CACHED ACTIVE BRANCHES ${since} TO ${until} ===`);
65130
66131 try {
67- // Step 1: Get all branches in the repository
68- console.log('*** STEP 1: LISTING ALL BRANCHES ***');
69- const branches = await github.paginate(github.rest.repos.listBranches, {
70- owner: context.repo.owner,
71- repo: context.repo.repo,
72- per_page: 100,
73- });
132+ // Get the list of active branches for the year (cached after first call)
133+ const activeBranches = await getActiveBranchesForYear();
74134
75- console.log(`*** FOUND ${branches.length} BRANCHES ***`);
76-
77- // Get default branch for main commit counting
135+ // Get default branch info
78136 const repo = await github.rest.repos.get({
79137 owner: context.repo.owner,
80138 repo: context.repo.repo,
81139 });
82140 const defaultBranch = repo.data.default_branch;
83- console.log(`*** DEFAULT BRANCH: ${defaultBranch} ***`);
84141
85- // Step 2: Check each branch for commits in the date range (with early exit)
86- console.log('*** STEP 2: FINDING BRANCHES WITH COMMITS IN DATE RANGE ***');
87- const activeBranches = [];
88- let branchesChecked = 0;
142+ console.log(`*** PROCESSING ${activeBranches.length} ACTIVE BRANCHES FOR THIS MONTH ***`);
89143
90- for (const branch of branches) {
91- try {
92- branchesChecked++;
93- if (branchesChecked % 20 === 0) {
94- console.log(`*** Checked ${branchesChecked}/${branches.length} branches ***`);
95- }
96-
97- // Get just the first page to check if branch has commits in date range
98- const branchCommits = await github.rest.repos.listCommits({
99- owner: context.repo.owner,
100- repo: context.repo.repo,
101- sha: branch.name,
102- since,
103- until,
104- per_page: 1, // Just check if any commits exist
105- });
106-
107- if (branchCommits.data.length > 0) {
108- activeBranches.push({
109- branch: branch,
110- hasCommits: true
111- });
112- console.log(`*** Branch ${branch.name} has commits in date range ***`);
113- }
114-
115- } catch (error) {
116- console.log(`*** Error checking ${branch.name}: ${error.message} ***`);
117- }
118-
119- // Small delay to avoid rate limiting
120- await new Promise(resolve => setTimeout(resolve, 50));
121- }
122-
123- console.log(`*** FOUND ${activeBranches.length} ACTIVE BRANCHES WITH COMMITS ***`);
124-
125- // Step 3: Get full commit data only from active branches
126- console.log('*** STEP 3: FETCHING ALL COMMITS FROM ACTIVE BRANCHES ***');
127144 const allCommitShas = new Set();
128145 const allAuthors = new Set();
129146 let mainBranchCommits = [];
130147
131- for (const activeBranch of activeBranches) {
148+ // Now only check the active branches for this specific month
149+ for (const branch of activeBranches) {
132150 try {
133- console.log(`*** Fetching all commits from active branch: ${activeBranch.branch.name} ***`);
134-
135151 const branchCommits = await github.paginate(github.rest.repos.listCommits, {
136152 owner: context.repo.owner,
137153 repo: context.repo.repo,
138- sha: activeBranch. branch.name,
154+ sha: branch.name,
139155 since,
140156 until,
141157 per_page: 100,
142158 });
143159
144- console.log(`*** Found ${branchCommits.length} commits on ${activeBranch.branch.name} ***`);
160+ if (branchCommits.length > 0) {
161+ console.log(`*** Found ${branchCommits.length} commits on ${branch.name} for this month ***`);
162+ }
145163
146164 // Add to our sets (automatically handles duplicates)
147165 branchCommits.forEach(commit => {
@@ -152,31 +170,28 @@ jobs:
152170 });
153171
154172 // Keep main branch commits separate
155- if (activeBranch. branch.name === defaultBranch) {
173+ if (branch.name === defaultBranch) {
156174 mainBranchCommits = branchCommits;
157175 }
158176
159177 } catch (error) {
160- console.log(`*** Error fetching commits from ${activeBranch. branch.name}: ${error.message} ***`);
178+ console.log(`*** Error fetching commits from ${branch.name}: ${error.message} ***`);
161179 }
162180
163- // Small delay to avoid rate limiting
164181 await new Promise(resolve => setTimeout(resolve, 100));
165182 }
166183
167- console.log(`*** PROCESSED ${activeBranches.length} ACTIVE BRANCHES ***`);
168- console.log(`*** TOTAL UNIQUE COMMITS ACROSS ALL ACTIVE BRANCHES: ${allCommitShas.size} ***`);
184+ console.log(`*** TOTAL UNIQUE COMMITS ACROSS ACTIVE BRANCHES: ${allCommitShas.size} ***`);
169185 console.log(`*** COMMITS ON MAIN BRANCH: ${mainBranchCommits.length} ***`);
170186 console.log(`*** TOTAL UNIQUE AUTHORS: ${allAuthors.size} ***`);
171187 console.log(`*** AUTHORS: ${Array.from(allAuthors).join(', ')} ***`);
172188
173- // Step 4: Get detailed stats from main branch commits
174- console.log('*** STEP 4: GETTING DETAILED STATS FROM MAIN BRANCH ***');
189+ // Get detailed stats from main branch commits
175190 let totalAdditions = 0;
176191 let totalDeletions = 0;
177192 let mainFileChanges = 0;
178193
179- for (const commit of mainBranchCommits.slice(0, 50)) { // Limit to avoid rate limits
194+ for (const commit of mainBranchCommits.slice(0, 50)) {
180195 try {
181196 const commitDetail = await github.rest.repos.getCommit({
182197 owner: context.repo.owner,
@@ -203,11 +218,11 @@ jobs:
203218 netLines: totalAdditions - totalDeletions,
204219 };
205220
206- console.log('*** FINAL OPTIMIZED RESULT ***', JSON.stringify(result, null, 2));
221+ console.log('*** CACHED OPTIMIZED RESULT ***', JSON.stringify(result, null, 2));
207222 return result;
208223
209224 } catch (error) {
210- console.log('*** ERROR WITH OPTIMIZED APPROACH ***', error.message);
225+ console.log('*** ERROR WITH CACHED APPROACH ***', error.message);
211226 return {
212227 totalCommitsAllBranches: 0,
213228 totalCommitsMain: 0,
@@ -311,7 +326,7 @@ jobs:
311326 </svg>`;
312327 }
313328
314- // Commit charts to repository
329+ // Commit charts to repository and create PR
315330 async function commitChartsToRepo(monthlyData, targetBranch) {
316331 const totalCommits = monthlyData.map(d => d.stats?.totalCommitsAllBranches || d.stats?.totalCommits || 0);
317332 const mergedPRs = monthlyData.map(d => d.prStats?.merged || 0);
@@ -326,13 +341,51 @@ jobs:
326341 const linesAddedSvg = createSVGChart(linesAdded, 'Total Lines Added', '#22c55e', monthLabels);
327342 const activeContributorsSvg = createSVGChart(activeContributors, 'Active Contributors', '#8b5cf6', monthLabels);
328343
344+ // Create a new branch for the PR
345+ const now = new Date();
346+ const prBranchName = `update-stats-charts-${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}`;
347+
348+ console.log(`*** CREATING NEW BRANCH: ${prBranchName} ***`);
349+
350+ try {
351+ // Get the default branch reference
352+ const defaultBranch = await github.rest.repos.get({
353+ owner: context.repo.owner,
354+ repo: context.repo.repo,
355+ });
356+
357+ const mainBranchRef = await github.rest.git.getRef({
358+ owner: context.repo.owner,
359+ repo: context.repo.repo,
360+ ref: `heads/${defaultBranch.data.default_branch}`,
361+ });
362+
363+ // Create new branch
364+ await github.rest.git.createRef({
365+ owner: context.repo.owner,
366+ repo: context.repo.repo,
367+ ref: `refs/heads/${prBranchName}`,
368+ sha: mainBranchRef.data.object.sha,
369+ });
370+
371+ console.log(`*** CREATED BRANCH: ${prBranchName} ***`);
372+
373+ } catch (error) {
374+ if (error.message.includes('Reference already exists')) {
375+ console.log(`*** BRANCH ${prBranchName} ALREADY EXISTS, USING EXISTING BRANCH ***`);
376+ } else {
377+ throw error;
378+ }
379+ }
380+
329381 const chartFiles = [
330- { path: 'docs/assets /total-commits-chart.svg', content: totalCommitsSvg },
331- { path: 'docs/assets /merged-prs-chart.svg', content: mergedPRsSvg },
332- { path: 'docs/assets /lines-added-chart.svg', content: linesAddedSvg },
333- { path: 'docs/assets /active-contributors-chart.svg', content: activeContributorsSvg }
382+ { path: 'docs/stats /total-commits-chart.svg', content: totalCommitsSvg },
383+ { path: 'docs/stats /merged-prs-chart.svg', content: mergedPRsSvg },
384+ { path: 'docs/stats /lines-added-chart.svg', content: linesAddedSvg },
385+ { path: 'docs/stats /active-contributors-chart.svg', content: activeContributorsSvg }
334386 ];
335387
388+ // Commit files to the new branch
336389 for (const file of chartFiles) {
337390 try {
338391 const base64Content = Buffer.from(file.content).toString('base64');
@@ -344,7 +397,7 @@ jobs:
344397 owner: context.repo.owner,
345398 repo: context.repo.repo,
346399 path: file.path,
347- ref: targetBranch
400+ ref: prBranchName
348401 });
349402 existingFileSha = existingFile.data.sha;
350403 } catch (error) {
@@ -357,7 +410,7 @@ jobs:
357410 path: file.path,
358411 message: `Update repository activity chart: ${file.path}`,
359412 content: base64Content,
360- branch: targetBranch
413+ branch: prBranchName
361414 };
362415
363416 if (existingFileSha) {
@@ -370,6 +423,53 @@ jobs:
370423 console.log(`Error updating ${file.path}:`, error.message);
371424 }
372425 }
426+
427+ // Create the PR
428+ const latestMonth = monthlyData[monthlyData.length - 1];
429+ const prTitle = `📊 Update Repository Statistics Charts - ${latestMonth.month}`;
430+ const prBody = `## 📊 Monthly Repository Statistics Update
431+
432+ This PR automatically updates our repository activity charts with the latest data for **${latestMonth.month}**.
433+
434+ ### 📈 Updated Charts
435+ - 🔵 **Total Commits (All Branches):** ${latestMonth.stats?.totalCommitsAllBranches || latestMonth.stats?.totalCommits || 0} commits
436+ - 🟢 **Merged Pull Requests:** ${latestMonth.prStats?.merged || 0} PRs
437+ - 🟣 **Total Lines Added:** ${(latestMonth.stats?.linesAdded || 0).toLocaleString()} lines
438+ - 🟠 **Active Contributors:** ${latestMonth.stats?.totalAuthors || latestMonth.stats?.uniqueContributors || 0} developers
439+
440+ ### 📁 Files Updated
441+ - \`docs/stats/total-commits-chart.svg\`
442+ - \`docs/stats/merged-prs-chart.svg\`
443+ - \`docs/stats/lines-added-chart.svg\`
444+ - \`docs/stats/active-contributors-chart.svg\`
445+
446+ ### 🤖 About
447+ This PR was automatically generated by our monthly statistics bot. The charts show 12 months of repository activity trends and can be embedded in documentation to showcase project health and development velocity.
448+
449+ ---
450+ *Generated automatically on ${new Date().toLocaleDateString()}*`;
451+
452+ try {
453+ const pullRequest = await github.rest.pulls.create({
454+ owner: context.repo.owner,
455+ repo: context.repo.repo,
456+ title: prTitle,
457+ head: prBranchName,
458+ base: defaultBranch.data.default_branch,
459+ body: prBody,
460+ });
461+
462+ console.log(`*** CREATED PR: ${pullRequest.data.html_url} ***`);
463+ return pullRequest.data.html_url;
464+
465+ } catch (error) {
466+ if (error.message.includes('A pull request already exists')) {
467+ console.log(`*** PR ALREADY EXISTS FOR BRANCH ${prBranchName} ***`);
468+ return `PR already exists for ${prBranchName}`;
469+ } else {
470+ throw error;
471+ }
472+ }
373473 }
374474
375475 // Main execution
@@ -418,9 +518,9 @@ jobs:
418518 await new Promise(resolve => setTimeout(resolve, 500));
419519 }
420520
421- // Create and commit SVG charts
521+ // Create and commit SVG charts, then create PR
422522 console.log('Generating and committing SVG charts...');
423- await commitChartsToRepo(monthlyDataArray, targetBranch);
523+ const prUrl = await commitChartsToRepo(monthlyDataArray, targetBranch);
424524
425525 // Get language stats
426526 const languageStats = await getLanguageStats();
@@ -429,7 +529,7 @@ jobs:
429529 const latestMonthData = monthlyDataArray[monthlyDataArray.length - 1];
430530 console.log(`*** LATEST MONTH FOR MATTERMOST ***`, JSON.stringify(latestMonthData, null, 2));
431531
432- // Post to Mattermost
532+ // Post to Mattermost with PR link
433533 const mattermostPayload = {
434534 text: `📊 **${latestMonthData.month} Repository Activity**`,
435535 attachments: [{
@@ -446,8 +546,9 @@ jobs:
446546 `**Top Contributors:** ${(latestMonthData.stats?.authorsList || []).slice(0, 3).join(', ') || 'None'}\n` +
447547 `**Languages:** ${(languageStats || []).map(l => `${l.language} (${l.percentage}%)`).join(', ') || 'N/A'}\n\n` +
448548 `*Excluding merges, ${latestMonthData.stats?.totalAuthors || 0} authors pushed ${latestMonthData.stats?.totalCommitsMain || 0} commits to main and ${latestMonthData.stats?.totalCommitsAllBranches || 0} commits to all branches*\n\n` +
549+ `**📊 Charts Updated:** ${typeof prUrl === 'string' && prUrl.startsWith('http') ? `[View PR](${prUrl})` : 'Charts updated in docs/stats/'}\n` +
449550 `Repository: ${context.repo.owner}/${context.repo.repo}`,
450- footer: 'Charts updated in docs/assets/ for documentation '
551+ footer: 'Monthly statistics generated automatically '
451552 }]
452553 };
453554
0 commit comments