mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-15 16:41:53 +00:00
feat(contributors): add contributors info
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -6,6 +6,7 @@ interface LatestReleaseBase {
|
||||
version: string;
|
||||
url: string;
|
||||
repoUrl: string;
|
||||
contributors: string[];
|
||||
}
|
||||
|
||||
export interface CuratedRelease extends LatestReleaseBase {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user