Skip to content

Commit 8f79e9b

Browse files
committed
feat(reports): add dedicated Homebrew Package Updates section
Created a hybrid presentation format for Homebrew tap version bumps: - Badge summary showing update counts per tap (production-tap first) - Quick summary table with tap names and counts - Collapsible details with package tables showing version progressions - Positioned as subsection under Development category - Separated from Bot Activity section for cleaner presentation Changes: - Add generateHomebrewUpdatesSection() with badges, table, and details - Add parseHomebrewPackageUpdates() to extract package names/versions - Add generatePackageTable() to format version progression tables - Split botActivity into homebrewActivity and otherBotActivity - Inject Homebrew section under Development category - Add horizontal rules between major sections for visual separation - Rename main-tap to production-tap to emphasize stability Format: #### Homebrew Package Updates [Production: 23] [Experimental: 28] 51 automated updates via GitHub Actions... ##### Quick Summary (table) <details> production-tap (23 updates) <details> experimental-tap (28 updates) Impact: Bot Activity section reduced from 291 to 240 PRs, with 51 Homebrew updates cleanly separated and formatted for readability.
1 parent 58dc62a commit 8f79e9b

File tree

1 file changed

+205
-7
lines changed

1 file changed

+205
-7
lines changed

scripts/lib/markdown-generator.mjs

Lines changed: 205 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@ import GitHubProfileCard from '@site/src/components/GitHubProfileCard';
111111
// Track displayed items to avoid duplicates across categories
112112
const displayedUrls = new Set();
113113

114+
// Split bot activity into homebrew and other
115+
const homebrewActivity = botActivity.filter(
116+
(activity) =>
117+
activity.repo === "ublue-os/homebrew-tap" ||
118+
activity.repo === "ublue-os/homebrew-experimental-tap",
119+
);
120+
const otherBotActivity = botActivity.filter(
121+
(activity) =>
122+
activity.repo !== "ublue-os/homebrew-tap" &&
123+
activity.repo !== "ublue-os/homebrew-experimental-tap",
124+
);
125+
114126
// Generate area sections with planned vs opportunistic subsections
115127
const areaSections = areaCategories
116128
.map(([categoryName, categoryLabels]) => {
@@ -131,10 +143,27 @@ import GitHubProfileCard from '@site/src/components/GitHubProfileCard';
131143
return `![${labelName}](https://img.shields.io/badge/${encodedName}-${color}?style=flat-square)`;
132144
})
133145
.join(" ");
134-
return `### ${cleanCategoryName}\n\n${labelBadges}\n\n${section}`;
146+
147+
// Add Homebrew updates as a subsection under Development category
148+
let fullSection = `### ${cleanCategoryName}\n\n${labelBadges}\n\n${section}`;
149+
if (
150+
(cleanCategoryName === "Development" ||
151+
categoryName.includes("Development")) &&
152+
homebrewActivity.length > 0
153+
) {
154+
const homebrewSection =
155+
generateHomebrewUpdatesSection(homebrewActivity);
156+
// Extract just the content (remove the ## heading and adjust remaining headings)
157+
const homebrewContent = homebrewSection
158+
.replace(/^## Homebrew Package Updates\n\n/, "")
159+
.replace(/^### /gm, "##### "); // Convert ### to ##### for proper nesting
160+
fullSection += `\n\n#### Homebrew Package Updates\n\n${homebrewContent}`;
161+
}
162+
163+
return fullSection;
135164
})
136165
.filter((section) => section)
137-
.join("\n\n");
166+
.join("\n\n---\n\n");
138167

139168
// Generate kind sections with planned vs opportunistic subsections
140169
const kindSections = kindCategories
@@ -158,13 +187,15 @@ import GitHubProfileCard from '@site/src/components/GitHubProfileCard';
158187
.join(" ");
159188
return `### ${cleanCategoryName}\n\n${labelBadges}\n\n${section}`;
160189
})
161-
.join("\n\n");
190+
.join("\n\n---\n\n");
162191

163192
// Combine with section headers
164193
const categorySections = `# Focus Area
165194
166195
${areaSections}
167196
197+
---
198+
168199
# Work by Type
169200
170201
${kindSections}`;
@@ -173,8 +204,8 @@ ${kindSections}`;
173204
const allItems = [...plannedItems, ...opportunisticItems];
174205
const uncategorizedSection = generateUncategorizedSection(allItems);
175206

176-
// Generate bot activity section
177-
const botSection = generateBotActivitySection(botActivity);
207+
// Generate bot activity section (non-homebrew only, since homebrew is now under Development)
208+
const botSection = generateBotActivitySection(otherBotActivity);
178209

179210
// Generate contributors section
180211
const contributorsSection = generateContributorsSection(
@@ -407,7 +438,172 @@ function generateUncategorizedSection(items) {
407438
return `- ${title} by [@\u200B${author}](https://github.com/${author}) in [#${number}](${url})`;
408439
});
409440

410-
return `## Other\n\n${lines.join("\n")}`;
441+
return `---\n\n## Other\n\n${lines.join("\n")}`;
442+
}
443+
444+
/**
445+
* Generate Homebrew package updates section
446+
* Hybrid format: Badge summary + compact table
447+
*
448+
* @param {Array} homebrewActivity - Array of {repo, bot, count, items}
449+
* @returns {string} Markdown section with badges and table
450+
*/
451+
function generateHomebrewUpdatesSection(homebrewActivity) {
452+
// Calculate totals by tap
453+
let experimentalCount = 0;
454+
let productionCount = 0;
455+
456+
homebrewActivity.forEach((activity) => {
457+
if (activity.repo === "ublue-os/homebrew-experimental-tap") {
458+
experimentalCount += activity.count;
459+
} else if (activity.repo === "ublue-os/homebrew-tap") {
460+
productionCount += activity.count;
461+
}
462+
});
463+
464+
const totalCount = experimentalCount + productionCount;
465+
466+
// Parse package updates from PR titles
467+
const packageUpdates = parseHomebrewPackageUpdates(homebrewActivity);
468+
469+
// Generate badges - production first to highlight it
470+
const badges = [];
471+
if (productionCount > 0) {
472+
badges.push(
473+
`![Production Tap](https://img.shields.io/badge/production--tap-${productionCount}%20updates-blue?style=flat-square)`,
474+
);
475+
}
476+
if (experimentalCount > 0) {
477+
badges.push(
478+
`![Experimental Tap](https://img.shields.io/badge/experimental--tap-${experimentalCount}%20updates-orange?style=flat-square)`,
479+
);
480+
}
481+
482+
// Generate summary table - production first
483+
const summaryTable = `| Tap | Updates |
484+
|-----|---------|
485+
| production-tap | ${productionCount} |
486+
| experimental-tap | ${experimentalCount} |`;
487+
488+
// Generate detailed package tables - production first
489+
let detailSections = "";
490+
491+
// Production tap details
492+
if (Object.keys(packageUpdates.production).length > 0) {
493+
const prodTable = generatePackageTable(
494+
packageUpdates.production,
495+
"production",
496+
);
497+
detailSections += `<details>
498+
<summary>View all production-tap updates (${productionCount})</summary>
499+
500+
${prodTable}
501+
502+
</details>`;
503+
}
504+
505+
// Experimental tap details
506+
if (Object.keys(packageUpdates.experimental).length > 0) {
507+
const expTable = generatePackageTable(
508+
packageUpdates.experimental,
509+
"experimental",
510+
);
511+
if (detailSections) detailSections += "\n\n";
512+
detailSections += `<details>
513+
<summary>View all experimental-tap updates (${experimentalCount})</summary>
514+
515+
${expTable}
516+
517+
</details>`;
518+
}
519+
520+
return `## Homebrew Package Updates
521+
522+
${badges.join(" ")}
523+
524+
**${totalCount} automated updates** this month via GitHub Actions. Homebrew tap version bumps ensure Bluefin users always have access to the latest stable releases.
525+
526+
### Quick Summary
527+
528+
${summaryTable}
529+
530+
${detailSections}`;
531+
}
532+
533+
/**
534+
* Parse package names and versions from homebrew PR titles
535+
*
536+
* @param {Array} homebrewActivity - Array of {repo, bot, count, items}
537+
* @returns {Object} { experimental: {pkg: [versions]}, production: {pkg: [versions]} }
538+
*/
539+
function parseHomebrewPackageUpdates(homebrewActivity) {
540+
const updates = {
541+
experimental: {},
542+
production: {},
543+
};
544+
545+
homebrewActivity.forEach((activity) => {
546+
const tapKey =
547+
activity.repo === "ublue-os/homebrew-experimental-tap"
548+
? "experimental"
549+
: "production";
550+
551+
activity.items.forEach((item) => {
552+
const title = item.content.title;
553+
// Pattern: "package-name version" or "package-name: version"
554+
// Example: "opencode-desktop-linux 1.1.18"
555+
const match = title.match(/^([a-z0-9-]+)\s+([0-9]+\.[0-9.]+)/i);
556+
557+
if (match) {
558+
const pkgName = match[1];
559+
const version = match[2];
560+
561+
if (!updates[tapKey][pkgName]) {
562+
updates[tapKey][pkgName] = [];
563+
}
564+
565+
updates[tapKey][pkgName].push({
566+
version,
567+
prNumber: item.content.number,
568+
prUrl: item.content.url,
569+
});
570+
}
571+
});
572+
});
573+
574+
return updates;
575+
}
576+
577+
/**
578+
* Generate package update table for a tap
579+
*
580+
* @param {Object} packages - {pkgName: [{version, prNumber, prUrl}]}
581+
* @param {string} tapName - "experimental" or "main"
582+
* @returns {string} Markdown table
583+
*/
584+
function generatePackageTable(packages, tapName) {
585+
// Sort packages by update count (descending)
586+
const sortedPackages = Object.entries(packages).sort(
587+
(a, b) => b[1].length - a[1].length,
588+
);
589+
590+
const rows = sortedPackages.map(([pkgName, versions]) => {
591+
// Show version progression or single version
592+
const versionStr =
593+
versions.length > 1
594+
? `${versions[0].version}${versions[versions.length - 1].version} (${versions.length} updates)`
595+
: versions[0].version;
596+
597+
// Link to first PR (or could link to all)
598+
const prLink = `[#${versions[0].prNumber}](${versions[0].prUrl})`;
599+
600+
return `| ${pkgName} | ${versionStr} | ${prLink} |`;
601+
});
602+
603+
const header = `| Package | Versions | PR |
604+
|---------|----------|-----|`;
605+
606+
return [header, ...rows].join("\n");
411607
}
412608

413609
/**
@@ -424,7 +620,9 @@ function generateBotActivitySection(botActivity) {
424620
const table = generateBotActivityTable(botActivity);
425621
const details = generateBotDetailsList(botActivity);
426622

427-
return `## 🤖 Bot Activity
623+
return `---
624+
625+
## 🤖 Bot Activity
428626
429627
${table}
430628

0 commit comments

Comments
 (0)