diff --git a/ui/actions/releases/releases.ts b/ui/actions/releases/releases.ts index f72b1b60de..7bda974fe3 100644 --- a/ui/actions/releases/releases.ts +++ b/ui/actions/releases/releases.ts @@ -10,14 +10,20 @@ const REPO = "prowler-cloud/prowler"; const REPO_URL = `https://github.com/${REPO}`; const RELEASES_ENDPOINT = `https://api.github.com/repos/${REPO}/releases?per_page=10`; const MAX_HIGHLIGHTS = 3; +const MAX_CONTRIBUTORS = 8; const REVALIDATE_SECONDS = 3600; const META_HEADINGS = [ /^what'?s changed$/i, /^new contributors$/i, + /^community contributors$/i, + /^contributors$/i, /^full changelog$/i, ]; +const CONTRIBUTOR_HEADING = /contributor/i; +const CONTRIBUTOR_HANDLE = /(?:^|[\s([])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/g; + const releaseSchema = z.object({ tag_name: z.string(), html_url: z.url(), @@ -41,9 +47,39 @@ const stripFencedAndDetails = (body: string) => .replace(/```[\s\S]*?```/g, "") .replace(//gi, ""); +const parseContributors = (clean: string): string[] => { + const sectionRe = /^##\s+(.+?)\s*$/gm; + const matches = Array.from(clean.matchAll(sectionRe)); + const seen = new Set(); + const ordered: string[] = []; + + for (let i = 0; i < matches.length; i++) { + const heading = matches[i][1].trim(); + if (!CONTRIBUTOR_HEADING.test(stripLeadingSymbols(heading))) continue; + + const start = matches[i].index! + matches[i][0].length; + const end = matches[i + 1]?.index ?? clean.length; + const section = clean.slice(start, end); + + for (const m of Array.from(section.matchAll(CONTRIBUTOR_HANDLE))) { + const handle = m[1]; + if (seen.has(handle.toLowerCase())) continue; + seen.add(handle.toLowerCase()); + ordered.push(handle); + if (ordered.length >= MAX_CONTRIBUTORS) return ordered; + } + } + + return ordered; +}; + const parseBody = ( body: string, -): { curated: string[]; components: ReleaseComponent[] } => { +): { + curated: string[]; + components: ReleaseComponent[]; + contributors: string[]; +} => { const clean = stripFencedAndDetails(body); const headings = Array.from(clean.matchAll(/^##\s+(.+?)\s*$/gm)).map((m) => m[1].trim(), @@ -58,7 +94,9 @@ const parseBody = ( headings.some((h) => h.toUpperCase() === comp), ); - return { curated, components }; + const contributors = parseContributors(clean); + + return { curated, components, contributors }; }; const buildHeaders = (): HeadersInit => { @@ -87,12 +125,13 @@ const fetchLatestReleaseWithHighlights = async (): Promise => { for (const release of releases) { if (release.draft || release.prerelease || !release.body) continue; - const { curated, components } = parseBody(release.body); + const { curated, components, contributors } = parseBody(release.body); const base = { version: release.tag_name.replace(/^v/, ""), url: release.html_url, repoUrl: REPO_URL, + contributors, }; if (curated.length > 0) { diff --git a/ui/actions/releases/types.ts b/ui/actions/releases/types.ts index fd94844b44..f463e9bfd1 100644 --- a/ui/actions/releases/types.ts +++ b/ui/actions/releases/types.ts @@ -6,6 +6,7 @@ interface LatestReleaseBase { version: string; url: string; repoUrl: string; + contributors: string[]; } export interface CuratedRelease extends LatestReleaseBase { diff --git a/ui/app/(auth)/layout.tsx b/ui/app/(auth)/layout.tsx index 07fe3a60c3..19f2d96ff2 100644 --- a/ui/app/(auth)/layout.tsx +++ b/ui/app/(auth)/layout.tsx @@ -46,10 +46,10 @@ export default function AuthLayout({ children }: { children: ReactNode }) { {children} - + ); diff --git a/ui/components/auth/oss/auth-release-highlights.tsx b/ui/components/auth/oss/auth-release-highlights.tsx index 16fa50da38..f417f3332e 100644 --- a/ui/components/auth/oss/auth-release-highlights.tsx +++ b/ui/components/auth/oss/auth-release-highlights.tsx @@ -1,3 +1,4 @@ +import { Tooltip } from "@heroui/tooltip"; import { Icon } from "@iconify/react"; import type { LatestRelease, ReleaseComponent } from "@/actions/releases/types"; @@ -91,6 +92,38 @@ export const AuthReleaseHighlights = ({

)} + {release.contributors.length > 0 && ( +
+

+ Community contributors +

+
    + {release.contributors.map((handle) => ( +
  • + + + + + +
  • + ))} +
+
+ )} +