diff --git a/README.md b/README.md index 60a0cd5..56a4898 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Building SpaceBadgers has been a labor of love, aiming to offer a superior, reli ## Development ### Environment Variables -> Paste this template into `badgers-web/.env.local` +> Paste this template into `badgers-web/.env.local` for local development ```py # Frontend Configuration @@ -53,7 +53,8 @@ NEXT_PUBLIC_WEB_PROTO = "http" # Web frontend protocol NEXT_PUBLIC_WEB_HOST = "127.0.0.1:3000" # Web frontend host # API Tokens -GITHUB_TOKEN = "ghp_Foo1234567" # Required for GitHub badges +GITHUB_TOKEN = "ghp_Foo1234567" # Required for GitHub badges (private token) +GITLAB_TOKEN = "glfoo-zKvC1234" # Required for GitLab badges (private token) CRATESIO_TOKEN = "cio51fdR1234567" # Required for crates.io badges ``` diff --git a/badgers-web/src/app/gitlab/license/[owner]/[repo]/route.ts b/badgers-web/src/app/gitlab/license/[owner]/[repo]/route.ts new file mode 100644 index 0000000..b228c72 --- /dev/null +++ b/badgers-web/src/app/gitlab/license/[owner]/[repo]/route.ts @@ -0,0 +1,21 @@ +import { NextRequest } from "next/server" + +import Badge from '@/utils/Badge' +import GitLab from '@/utils/GitLab' + +interface Params { + params: { + owner: string + repo: string + } +} + +export async function GET(request: NextRequest, { params: { owner, repo } }: Params) { + const repoData = await GitLab.getClient().getRepository({ owner, repo, license: true }) + const licenseName = repoData?.license?.nickname ?? repoData?.license?.key + return await Badge.generate(request, 'license', licenseName ?? 'unknown', { + color: !!licenseName ? 'blue' : 'gray' + }) +} + +export const runtime = 'edge' diff --git a/badgers-web/src/app/gitlab/release/[owner]/[repo]/route.ts b/badgers-web/src/app/gitlab/release/[owner]/[repo]/route.ts new file mode 100644 index 0000000..e7a3ad6 --- /dev/null +++ b/badgers-web/src/app/gitlab/release/[owner]/[repo]/route.ts @@ -0,0 +1,23 @@ +import { NextRequest } from "next/server" + +import Badge from '@/utils/Badge' +import GitLab from '@/utils/GitLab' + +interface Params { + params: { + owner: string + repo: string + } +} + +export async function GET(request: NextRequest, { params: { owner, repo } }: Params) { + const release = await GitLab.getClient().getLatestRelease({ owner, repo }) + const shortestName = [release?.tag_name, release?.name] + .filter(Boolean) + .reduce((a, b) => a!.length < b!.length ? a : b) + return await Badge.generate(request, 'release', shortestName ?? 'None', { + color: !!shortestName ? 'blue' : 'yellow' + }) +} + +export const runtime = 'edge' diff --git a/badgers-web/src/app/page.tsx b/badgers-web/src/app/page.tsx index 9fc9e93..edb88a0 100644 --- a/badgers-web/src/app/page.tsx +++ b/badgers-web/src/app/page.tsx @@ -145,6 +145,19 @@ export default function Home() { +
+
+ + + + + + + + + +
+
diff --git a/badgers-web/src/utils/GitLab.ts b/badgers-web/src/utils/GitLab.ts new file mode 100644 index 0000000..6b5a17b --- /dev/null +++ b/badgers-web/src/utils/GitLab.ts @@ -0,0 +1,89 @@ +// Supports GitLab API v4 + +/** + * GitLab API base url + */ +const API_BASE = 'https://gitlab.com/api/v4'; + +type ProjectInfo = { + owner: string + repo: string +} + +type GetRepositoryArgs = ProjectInfo & { + license: boolean +} + +/** + * A subset of the GitLab repository response. + * + * @see https://docs.gitlab.com/api/projects.html#get-single-project + */ +type GetRepositoryResponse = { + id: number + default_branch: string + name: string + license: { + key: string + name: string + nickname: string + } + forks_count: number + star_count: number +} + +type Release = { + name: string + tag_name: string + upcoming_release: boolean +} + +/** + * GitLab API client + */ +class GitLabClient { + privateToken: string + + constructor(privateToken: string) { + this.privateToken = privateToken + } + + /** + * Build a GitLab API url with authentication. + */ + buildUrl(path: string, query: Record = {}): string { + const queryArgs = { + ...query, + private_token: this.privateToken, + } + const queryString = Object + .entries(queryArgs) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join('&') + return `${API_BASE}/${path}?${queryString}` + } + + async getRepository({ owner, repo, license }: GetRepositoryArgs): Promise { + const repoId = encodeURIComponent(`${owner}/${repo}`) + const resp = await fetch(this.buildUrl(`projects/${repoId}`, { license })) + if (resp.status !== 200) return null + return await resp.json() as GetRepositoryResponse + } + + async getLatestRelease({ owner, repo }: ProjectInfo): Promise { + const repoId = encodeURIComponent(`${owner}/${repo}`) + const resp = await fetch(this.buildUrl(`projects/${repoId}/releases`)) + if (resp.status !== 200) return null + const releases = await resp.json() as Array + if (releases.length === 0) return null + const latestNonUpcomingRelease = releases.find(r => !r.upcoming_release) + if (latestNonUpcomingRelease === undefined) return null + return latestNonUpcomingRelease + } +} + +export default class GitLab { + static getClient(): GitLabClient { + return new GitLabClient(process.env.GITLAB_TOKEN as string); + } +}