feat: show GitHub commit history in topbar notifications

This commit is contained in:
KodekaTeam committed 2026-06-22 21:22:02 +07:00
1 parent 1435f5a6d7
commit 021163cf44
4 files changed
+388 -239

No files matched your search

+8 -4
View File
@@ -1,5 +1,5 @@
import "dotenv/config";
import Fastify from "fastify";
import Fastify, { FastifyError } from "fastify";
import cors from "@fastify/cors";
import jwt from "@fastify/jwt";
import rateLimit from "@fastify/rate-limit";
@@ -25,6 +25,7 @@ import { organizationsRoutes } from "./routes/organizations";
import { projectsRoutes } from "./routes/projects";
import { gitProviderRoutes } from "./routes/git-providers";
import { storageDestinationRoutes } from "./routes/storage-destinations";
import { commitHistoryRoutes } from "./routes/commit-history";
const PORT = parseInt(process.env.PORT || "4000");
const HOST = process.env.HOST || "0.0.0.0";
@@ -177,14 +178,17 @@ async function start() {
await app.register(storageDestinationRoutes, {
prefix: `${API_PREFIX}/storage-destinations`,
});
await app.register(commitHistoryRoutes, {
prefix: `${API_PREFIX}/notifications`,
});
// ── Error handler ─────────────────────────────────────
app.setErrorHandler((error: any, _req, reply) => {
app.setErrorHandler((error: FastifyError, _req, reply) => {
app.log.error(error);
const code = (error as any).statusCode || 500;
const code = error.statusCode || 500;
reply.status(code).send({
success: false,
error: (error as any).message || "Internal Server Error",
error: error.message || "Internal Server Error",
code,
});
});
+188
View File
@@ -0,0 +1,188 @@
import { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth";
const REPOSITORY = "DoktainerApp/doktainer";
const BRANCH = "main";
const PATH_FILTER = "";
const MAX_COMMITS = 5;
const CACHE_TTL_MS = 5 * 60 * 1000;
type GithubCommitPayload = {
sha?: unknown;
html_url?: unknown;
commit?: {
message?: unknown;
author?: {
name?: unknown;
date?: unknown;
};
};
author?: {
login?: unknown;
avatar_url?: unknown;
html_url?: unknown;
} | null;
};
type CommitHistoryItem = {
id: string;
sha: string;
shortSha: string;
title: string;
message: string;
authorName: string;
authorUsername: string;
authorAvatarUrl: string | null;
url: string;
committedAt: string;
};
type CommitHistoryCache = {
expiresAt: number;
payload: {
success: true;
data: CommitHistoryItem[];
meta: {
repository: string;
branch: string;
path: string;
};
};
};
let cache: CommitHistoryCache | null = null;
function normalizeRepository(value: string) {
return value.replace(/^https:\/\/github\.com\//i, "").replace(/\.git$/i, "");
}
function getCommitHistoryConfig() {
return {
repository: normalizeRepository(REPOSITORY),
branch: BRANCH,
path: PATH_FILTER,
};
}
function getFirstLine(value: unknown) {
return String(value ?? "")
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean);
}
function mapGithubCommit(item: GithubCommitPayload): CommitHistoryItem | null {
const sha = String(item.sha ?? "").trim();
const committedAt = String(item.commit?.author?.date ?? "").trim();
const title = getFirstLine(item.commit?.message);
if (!sha || !committedAt || !title) {
return null;
}
const authorName = String(
item.author?.login ?? item.commit?.author?.name ?? "Unknown author",
);
const shortSha = sha.slice(0, 7);
return {
id: sha,
sha,
shortSha,
title,
message: `${authorName} committed ${shortSha}`,
authorName: String(item.commit?.author?.name ?? authorName),
authorUsername: String(item.author?.login ?? ""),
authorAvatarUrl:
typeof item.author?.avatar_url === "string"
? item.author.avatar_url
: null,
url: String(item.html_url ?? ""),
committedAt,
};
}
async function fetchCommitHistory() {
const config = getCommitHistoryConfig();
const [owner, repo] = config.repository.split("/");
if (!owner || !repo) {
throw new Error(
"Commit history repository must use the owner/repository format",
);
}
const url = new URL(
`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`,
);
url.searchParams.set("sha", config.branch);
url.searchParams.set("per_page", String(MAX_COMMITS));
if (config.path) {
url.searchParams.set("path", config.path);
}
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"User-Agent": "Doktainer",
"X-GitHub-Api-Version": "2022-11-28",
};
const response = await fetch(url, { headers });
const text = await response.text();
if (!response.ok) {
let message = text.trim();
try {
const payload = JSON.parse(text) as { message?: unknown };
message = String(payload.message ?? message);
} catch {
// Keep the plain response body.
}
throw new Error(
message || `GitHub request failed with status ${response.status}`,
);
}
const payload = JSON.parse(text) as unknown;
const commits = Array.isArray(payload)
? payload
.map((item) => mapGithubCommit(item as GithubCommitPayload))
.filter((item): item is CommitHistoryItem => Boolean(item))
: [];
return {
success: true as const,
data: commits,
meta: {
repository: config.repository,
branch: config.branch,
path: config.path,
},
};
}
export async function commitHistoryRoutes(app: FastifyInstance) {
app.get("/commits", { preHandler: authenticate }, async (_req, reply) => {
try {
if (cache && cache.expiresAt > Date.now()) {
return cache.payload;
}
const payload = await fetchCommitHistory();
cache = {
expiresAt: Date.now() + CACHE_TTL_MS,
payload,
};
return payload;
} catch (error) {
reply.status(502).send({
success: false,
error:
error instanceof Error
? error.message
: "Failed to load commit history",
});
}
});
}