Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion scripts/fetch-github-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ const AUTOMATION_LOGIN_PATTERNS = [
const isAutomationAccount = (login) =>
AUTOMATION_LOGIN_PATTERNS.some((pattern) => pattern.test(login));

// A snapshot older than this gets a loud build warning. It is not a build
// failure on purpose: a transient API or rate-limit failure must not block a
// deploy. Visibility is the fix — the community section renders updatedAt.
const STALE_AFTER_DAYS = 7;

const ageInDays = (isoDate) => {
const then = new Date(isoDate).getTime();
if (Number.isNaN(then)) return null;
return Math.floor((Date.now() - then) / 86400000);
};

async function fetchGitHubData() {
console.log('Fetching GitHub repository data at build time...');

Expand All @@ -38,6 +49,13 @@ async function fetchGitHubData() {
openIssues: '5+',
closedIssues: '20+'
};
// Timestamp of the data itself. Advanced only when this run really refreshes
// it, so a failed fetch cannot make a stale snapshot look fresh.
let updatedAt = new Date().toISOString();
// Whether this run replaced the preserved snapshot with fresh API data.
let contributorsOk = false;
let statsOk = false;
let refreshed = false;

// Preserve existing data if available
if (fs.existsSync(OUTPUT_FILE)) {
Expand All @@ -49,6 +67,9 @@ async function fetchGitHubData() {
if (existing.stats) {
stats = existing.stats;
}
if (existing.updatedAt) {
updatedAt = existing.updatedAt;
}
} catch (e) {
// Ignore read errors
}
Expand All @@ -68,6 +89,7 @@ async function fetchGitHubData() {
avatar_url: user.avatar_url,
html_url: user.html_url
}));
contributorsOk = true;
}
} else {
console.warn(`Contributors API returned ${contribRes.status}`);
Expand All @@ -92,14 +114,35 @@ async function fetchGitHubData() {
const closedIssues = await closedIssuesRes.json();
if (closedIssues.total_count !== undefined) stats.closedIssues = String(closedIssues.total_count);
}

statsOk = prsRes.ok && openIssuesRes.ok && closedIssuesRes.ok;
} catch (error) {
console.warn('Warning: Error fetching GitHub data at build time, using fallback values:', error.message);
}

refreshed = contributorsOk && statsOk;
if (refreshed) {
updatedAt = new Date().toISOString();
} else {
const age = ageInDays(updatedAt);
console.warn(
`[github-data] Refresh failed — the site will render the snapshot from ${updatedAt}` +
(age === null ? '.' : age > 0 ? ` (${age} day(s) old).` : ' (today).')
);
if (age !== null && age > STALE_AFTER_DAYS) {
console.warn(
`[github-data] That snapshot is past the ${STALE_AFTER_DAYS}-day threshold. ` +
'The community section shows its date, so the staleness is visible on the site.'
);
}
}

const payload = {
contributors,
stats,
updatedAt: new Date().toISOString()
updatedAt,
// true when the numbers above come from a previous run instead of this one
stale: !refreshed
};

fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true });
Expand Down
10 changes: 10 additions & 0 deletions website/src/components/CommunitySection/MonthlyPulse.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@
letter-spacing: 0.5px;
}

/* Timestamp of the snapshot the numbers come from — pulls up into the space
the grid reserves below itself so it reads as a caption to the stats. */
.dataAge {
margin: -1.5rem 0 0;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.65);
position: relative;
z-index: 2;
}

@media (max-width: 480px) {
.pulseGrid {
gap: 1rem;
Expand Down
28 changes: 28 additions & 0 deletions website/src/components/CommunitySection/MonthlyPulse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,34 @@ import React from 'react';
import styles from './MonthlyPulse.module.css';
import githubData from '@site/src/data/githubData.json';

const MONTHS = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];

/**
* Format the snapshot timestamp in UTC from the ISO string.
* Doing it by hand instead of toLocaleDateString keeps the server-rendered and
* client-rendered output identical, so the date can never mismatch on hydration.
*/
function formatSnapshotDate(updatedAt?: string): string | null {
if (!updatedAt) return null;
const snapshot = new Date(updatedAt);
if (Number.isNaN(snapshot.getTime())) return null;
return `${snapshot.getUTCDate()} ${MONTHS[snapshot.getUTCMonth()]} ${snapshot.getUTCFullYear()}`;
}

export default function MonthlyPulse() {
const stats = {
mergedPRs: githubData.stats?.mergedPRs || '15+',
openIssues: githubData.stats?.openIssues || '5+',
closedIssues: githubData.stats?.closedIssues || '20+'
};

const snapshotDate = formatSnapshotDate(githubData.updatedAt);
// The committed snapshot predates the `stale` field, so read it defensively.
const isStale = (githubData as { stale?: boolean }).stale === true;

return (
<div className={styles.pulseCard}>
<div className={styles.pulseHeader}>
Expand All @@ -30,6 +51,13 @@ export default function MonthlyPulse() {
<span className={styles.statLabel}>New Issues</span>
</div>
</div>

{snapshotDate && (
<p className={styles.dataAge}>
Data as of {snapshotDate}
{isStale ? ' — GitHub API unavailable at build time' : ''}
</p>
)}
</div>
);
}
Loading