feat(contributors): add contributors info

This commit is contained in:
pedrooot
2026-04-23 12:59:08 +02:00
parent 65e2d3b2d5
commit df2f7312db
5 changed files with 80 additions and 7 deletions
+42 -3
View File
@@ -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(/<details[\s\S]*?<\/details>/gi, "");
const parseContributors = (clean: string): string[] => {
const sectionRe = /^##\s+(.+?)\s*$/gm;
const matches = Array.from(clean.matchAll(sectionRe));
const seen = new Set<string>();
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<LatestRelease> => {
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) {
+1
View File
@@ -6,6 +6,7 @@ interface LatestReleaseBase {
version: string;
url: string;
repoUrl: string;
contributors: string[];
}
export interface CuratedRelease extends LatestReleaseBase {
+3 -3
View File
@@ -46,10 +46,10 @@ export default function AuthLayout({ children }: { children: ReactNode }) {
</Suspense>
{children}
<Toaster />
<GoogleTagManager
gtmId={process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID || ""}
/>
</Providers>
<GoogleTagManager
gtmId={process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID || ""}
/>
</body>
</html>
);
@@ -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 = ({
</p>
)}
{release.contributors.length > 0 && (
<div className="flex flex-col gap-2">
<p className="text-xs font-semibold tracking-wide text-slate-600 uppercase dark:text-white/70">
Community contributors
</p>
<ul className="flex flex-wrap items-center">
{release.contributors.map((handle) => (
<li key={handle} className="-ml-2 first:ml-0">
<Tooltip content={`@${handle}`} placement="top" shadow="sm">
<a
href={`https://github.com/${handle}`}
target="_blank"
rel="noopener noreferrer"
aria-label={`@${handle} on GitHub`}
className="block rounded-full ring-2 ring-white/80 transition-transform hover:z-10 hover:scale-110 focus-visible:z-10 focus-visible:ring-emerald-400/80 focus-visible:outline-none dark:ring-black/60"
>
<img
src={`https://github.com/${handle}.png?size=80`}
alt=""
width={32}
height={32}
loading="lazy"
className="h-8 w-8 rounded-full bg-slate-200 object-cover dark:bg-white/10"
/>
</a>
</Tooltip>
</li>
))}
</ul>
</div>
)}
<div className="flex flex-wrap items-center gap-4">
<Button
asChild
+1 -1
View File
@@ -11,7 +11,7 @@ const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://www.googletagmanager.com https://browser.sentry-cdn.com;
connect-src 'self' https://api.iconify.design https://api.simplesvg.com https://api.unisvg.com https://js.stripe.com https://www.googletagmanager.com https://*.sentry.io https://*.ingest.sentry.io;
img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com https://github.com https://avatars.githubusercontent.com;
font-src 'self';
style-src 'self' 'unsafe-inline';
frame-src 'self' https://js.stripe.com https://www.googletagmanager.com;