Files
indiekit-blog/theme/_data/githubActivity.js

292 lines
8.0 KiB
JavaScript

/**
* GitHub Activity Data
* Fetches from Indiekit's endpoint-github public API
* Falls back to direct GitHub API if Indiekit is unavailable
*/
import EleventyFetch from "@11ty/eleventy-fetch";
const GITHUB_USERNAME = process.env.GITHUB_USERNAME || "";
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
// Fallback featured repos if Indiekit API unavailable (from env: comma-separated)
const FALLBACK_FEATURED_REPOS = process.env.GITHUB_FEATURED_REPOS?.split(",").filter(Boolean) || [];
/**
* Fetch from Indiekit's public GitHub API endpoint
*/
async function fetchFromIndiekit(endpoint) {
const urls = [
`${INDIEKIT_URL}/github/api/${endpoint}`,
`${INDIEKIT_URL}/githubapi/api/${endpoint}`,
];
for (const url of urls) {
try {
console.log(`[githubActivity] Fetching from Indiekit: ${url}`);
const data = await EleventyFetch(url, {
duration: "15m",
type: "json",
});
console.log(`[githubActivity] Indiekit ${endpoint} success via ${url}`);
return data;
} catch (error) {
console.log(
`[githubActivity] Indiekit API unavailable for ${endpoint} at ${url}: ${error.message}`
);
}
}
return null;
}
/**
* Fetch from GitHub API directly
*/
async function fetchFromGitHub(endpoint) {
const url = `https://api.github.com${endpoint}`;
const headers = {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Eleventy-Site",
};
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
return await EleventyFetch(url, {
duration: "15m",
type: "json",
fetchOptions: { headers },
});
}
/**
* Truncate text with ellipsis
*/
function truncate(text, maxLength = 80) {
if (!text || text.length <= maxLength) return text || "";
return text.slice(0, maxLength - 1) + "...";
}
/**
* Extract commits from push events
*/
function extractCommits(events) {
if (!Array.isArray(events)) return [];
return events
.filter((event) => event.type === "PushEvent")
.flatMap((event) =>
(event.payload?.commits || []).map((commit) => ({
sha: commit.sha.slice(0, 7),
message: truncate(commit.message.split("\n")[0]),
url: `https://github.com/${event.repo.name}/commit/${commit.sha}`,
repo: event.repo.name,
repoUrl: `https://github.com/${event.repo.name}`,
date: event.created_at,
}))
)
.slice(0, 10);
}
/**
* Extract PRs/Issues from events
*/
function extractContributions(events) {
if (!Array.isArray(events)) return [];
return events
.filter(
(event) =>
(event.type === "PullRequestEvent" || event.type === "IssuesEvent") &&
event.payload?.action === "opened"
)
.map((event) => {
const item = event.payload.pull_request || event.payload.issue;
return {
type: event.type === "PullRequestEvent" ? "pr" : "issue",
title: truncate(item?.title),
url: item?.html_url,
repo: event.repo.name,
repoUrl: `https://github.com/${event.repo.name}`,
number: item?.number,
date: event.created_at,
};
})
.slice(0, 10);
}
/**
* Format starred repos
*/
function formatStarred(repos) {
if (!Array.isArray(repos)) return [];
return repos.map((repo) => ({
name: repo.full_name,
description: truncate(repo.description, 120),
url: repo.html_url,
stars: repo.stargazers_count,
language: repo.language,
topics: repo.topics?.slice(0, 5) || [],
}));
}
/**
* Fetch featured repos directly from GitHub (fallback)
*/
async function fetchFeaturedFromGitHub(repoList) {
const featured = [];
for (const repoFullName of repoList) {
try {
const repo = await fetchFromGitHub(`/repos/${repoFullName}`);
let commits = [];
try {
const commitsData = await fetchFromGitHub(
`/repos/${repoFullName}/commits?per_page=5`
);
commits = commitsData.map((c) => ({
sha: c.sha.slice(0, 7),
message: truncate(c.commit.message.split("\n")[0]),
url: c.html_url,
date: c.commit.author.date,
}));
} catch (e) {
console.log(`[githubActivity] Could not fetch commits for ${repoFullName}`);
}
featured.push({
fullName: repo.full_name,
name: repo.name,
description: repo.description,
url: repo.html_url,
stars: repo.stargazers_count,
forks: repo.forks_count,
language: repo.language,
isPrivate: repo.private,
commits,
});
} catch (error) {
console.log(`[githubActivity] Could not fetch ${repoFullName}: ${error.message}`);
}
}
return featured;
}
/**
* Fetch commits directly from user's recently pushed repos
* Fallback when events API doesn't include commit details
*/
async function fetchCommitsFromRepos(username, limit = 10) {
try {
const repos = await fetchFromGitHub(
`/users/${username}/repos?sort=pushed&per_page=5`
);
if (!Array.isArray(repos) || repos.length === 0) {
return [];
}
const allCommits = [];
for (const repo of repos.slice(0, 5)) {
try {
const repoCommits = await fetchFromGitHub(
`/repos/${repo.full_name}/commits?per_page=5`
);
for (const c of repoCommits) {
allCommits.push({
sha: c.sha.slice(0, 7),
message: truncate(c.commit?.message?.split("\n")[0]),
url: c.html_url,
repo: repo.full_name,
repoUrl: repo.html_url,
date: c.commit?.author?.date,
});
}
} catch {
// Skip repos we can't access
}
}
// Sort by date and limit
return allCommits
.sort((a, b) => new Date(b.date) - new Date(a.date))
.slice(0, limit);
} catch (error) {
console.log(`[githubActivity] Could not fetch commits from repos: ${error.message}`);
return [];
}
}
export default async function () {
try {
console.log("[githubActivity] Fetching GitHub data...");
// Try Indiekit public API first
const [indiekitStars, indiekitCommits, indiekitContributions, indiekitActivity, indiekitFeatured] =
await Promise.all([
fetchFromIndiekit("stars"),
fetchFromIndiekit("commits"),
fetchFromIndiekit("contributions"),
fetchFromIndiekit("activity"),
fetchFromIndiekit("featured"),
]);
// Check if Indiekit API is available
const hasIndiekitData =
indiekitStars?.stars ||
indiekitCommits?.commits ||
indiekitFeatured?.featured;
if (hasIndiekitData) {
console.log("[githubActivity] Using Indiekit API data");
return {
stars: indiekitStars?.stars || [],
commits: indiekitCommits?.commits || [],
contributions: indiekitContributions?.contributions || [],
activity: indiekitActivity?.activity || [],
featured: indiekitFeatured?.featured || [],
source: "indiekit",
};
}
// Fallback to direct GitHub API
console.log("[githubActivity] Falling back to GitHub API");
const [events, starred, featured] = await Promise.all([
fetchFromGitHub(`/users/${GITHUB_USERNAME}/events/public?per_page=50`),
fetchFromGitHub(`/users/${GITHUB_USERNAME}/starred?per_page=20&sort=created`),
fetchFeaturedFromGitHub(FALLBACK_FEATURED_REPOS),
]);
// Try to extract commits from events first
let commits = extractCommits(events || []);
// If events API didn't have commits, fetch directly from repos
if (commits.length === 0 && GITHUB_USERNAME) {
console.log("[githubActivity] Events API returned no commits, fetching from repos");
commits = await fetchCommitsFromRepos(GITHUB_USERNAME, 10);
}
return {
stars: formatStarred(starred || []),
commits,
contributions: extractContributions(events || []),
featured,
source: "github",
};
} catch (error) {
console.error("[githubActivity] Error:", error.message);
return {
stars: [],
commits: [],
contributions: [],
featured: [],
source: "error",
};
}
}