From 10c7ba42404bfac46181eedd4a03ad195ebcaf52 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Thu, 30 Jan 2025 22:45:22 -0600 Subject: [PATCH] Add git sync status for forked repos --- src/admin/github/GitHubForkStatusBadge.tsx | 14 +++ .../github/GitHubForkStatusBadgeClient.tsx | 74 +++++++++++++++ .../github/GitHubForkStatusBadgeServer.tsx | 27 ++++++ src/admin/github/index.ts | 90 +++++++++++++++++++ src/app/admin/configuration/page.tsx | 8 +- src/site/SiteChecklist.tsx | 2 +- src/site/SiteChecklistClient.tsx | 47 ++++------ src/site/SiteChecklistServer.tsx | 12 +-- src/site/config.ts | 17 ++-- src/site/globals.css | 3 + src/utility/github.ts | 32 ------- 11 files changed, 244 insertions(+), 82 deletions(-) create mode 100644 src/admin/github/GitHubForkStatusBadge.tsx create mode 100644 src/admin/github/GitHubForkStatusBadgeClient.tsx create mode 100644 src/admin/github/GitHubForkStatusBadgeServer.tsx create mode 100644 src/admin/github/index.ts delete mode 100644 src/utility/github.ts diff --git a/src/admin/github/GitHubForkStatusBadge.tsx b/src/admin/github/GitHubForkStatusBadge.tsx new file mode 100644 index 00000000..f4cb64f7 --- /dev/null +++ b/src/admin/github/GitHubForkStatusBadge.tsx @@ -0,0 +1,14 @@ +import { Suspense } from 'react'; +import GitHubForkStatusBadgeClient from './GitHubForkStatusBadgeClient'; +import GitHubForkStatusBadgeServer from './GitHubForkStatusBadgeServer'; +import { IS_DEVELOPMENT, IS_VERCEL_GIT_PROVIDER_GITHUB } from '@/site/config'; + +export default function GitHubForkStatusBadge() { + return IS_DEVELOPMENT + ? + : IS_VERCEL_GIT_PROVIDER_GITHUB + ? + + + : null; +} diff --git a/src/admin/github/GitHubForkStatusBadgeClient.tsx b/src/admin/github/GitHubForkStatusBadgeClient.tsx new file mode 100644 index 00000000..baf90acc --- /dev/null +++ b/src/admin/github/GitHubForkStatusBadgeClient.tsx @@ -0,0 +1,74 @@ +import Spinner from '@/components/Spinner'; +import clsx from 'clsx/lite'; +import Link from 'next/link'; +import { BiLogoGithub } from 'react-icons/bi'; + +export default function GitHubForkStatusBadgeClient({ + url, + label, + style = 'mono', + title, +}: { + url?: string + label?: string + style?: 'success' | 'warning' | 'mono' + title?: string +}) { + const classNameForStyle = () => { + switch (style) { + case 'success': return clsx( + 'text-green-700 hover:text-green-700', + 'dark:text-green-400 dark:hover:text-green-400', + 'bg-green-100/75 dark:bg-green-900/50', + 'border-green-300/25', + ); + case 'warning': return clsx( + 'text-amber-700 hover:text-amber-700', + 'dark:text-amber-400 dark:hover:text-amber-400', + 'bg-amber-100/75 dark:bg-amber-900/50', + 'border-amber-300/25 dark:border-amber-900', + ); + default: return clsx( + 'text-gray-700 hover:text-gray-700', + 'dark:text-gray-300 dark:hover:text-gray-300', + 'bg-gray-100/75 dark:bg-gray-900/50', + 'border-main', + ); + } + }; + + const className = clsx( + 'inline-flex items-center gap-2', + 'border transition-colors', + url ? 'hover:underline' : 'select-none', + 'pl-[4.5px] pr-2 py-[3px]', + 'rounded-full', + classNameForStyle(), + ); + + const content = <> + {!label + ? + : } + {label ?? 'Checking'} + ; + + return url + ? + {content} + + : + {content} + ; +} diff --git a/src/admin/github/GitHubForkStatusBadgeServer.tsx b/src/admin/github/GitHubForkStatusBadgeServer.tsx new file mode 100644 index 00000000..a6cc6f3e --- /dev/null +++ b/src/admin/github/GitHubForkStatusBadgeServer.tsx @@ -0,0 +1,27 @@ +import GitHubForkStatusBadgeClient from './GitHubForkStatusBadgeClient'; +import { + VERCEL_GIT_REPO_OWNER, + VERCEL_GIT_REPO_SLUG, +} from '@/site/config'; +import { getGitHubMeta } from '.'; + +export default async function GitHubForkStatusBadgeServer() { + const owner = VERCEL_GIT_REPO_OWNER; + const repo = VERCEL_GIT_REPO_SLUG; + + const { + url, + label, + title, + isBehind, + } = await getGitHubMeta({ owner, repo }); + + return ( + + ); +} diff --git a/src/admin/github/index.ts b/src/admin/github/index.ts new file mode 100644 index 00000000..395c6668 --- /dev/null +++ b/src/admin/github/index.ts @@ -0,0 +1,90 @@ +const BASE_OWNER = 'sambecker'; +const BASE_REPO = 'exif-photo-blog'; + +interface RepoParams { + owner?: string + repo?: string + branch?: string +}; + +// Urls + +const getGitHubRepoUrl = ({ + owner = BASE_OWNER, + repo = BASE_REPO, +}: RepoParams = {}) => + `https://github.com/${owner}/${repo}`; + +const getGitHubApiRepoUrl = ({ + owner = BASE_OWNER, + repo = BASE_REPO, +}: RepoParams = {}) => + `https://api.github.com/repos/${owner}/${repo}`; + +const getGitHubApiCommitsUrl = (params?: RepoParams) => + `${getGitHubApiRepoUrl(params)}/commits/main`; + +// Fetching + +const getGitHubApiCompareUrl = ({ + owner, + repo, + branch = 'main', +}: RepoParams = {}) => + `${getGitHubApiRepoUrl()}/compare/main...${owner}:${repo}:${branch}`; + +const getLatestBaseRepoCommitSha = async () => { + const response = await fetch(getGitHubApiCommitsUrl()); + const data = await response.json(); + return data.sha.slice(0, 7) as string; +}; + +const getIsRepoForkedFromBase = async (params: RepoParams) => { + const response = await fetch(getGitHubApiRepoUrl(params)); + const data = await response.json(); + return data.fork && data.source?.full_name === `${BASE_OWNER}/${BASE_REPO}`; +}; + +const getGitHubCommitsBehind = async (params?: RepoParams) => { + const response = await fetch(getGitHubApiCompareUrl(params)); + const data = await response.json(); + return data.behind_by as number; +}; + +const isRepoBaseRepo = async ({ owner, repo }: RepoParams) => + owner?.toLowerCase() === BASE_OWNER && + repo?.toLowerCase() === BASE_REPO; + +export const getGitHubMeta = async (params: RepoParams) => { + const [ + url, + isForkedFromBase, + isBaseRepo, + latestBaseRepoCommitSha, + behindBy, + ] = await Promise.all([ + getGitHubRepoUrl(params), + getIsRepoForkedFromBase(params), + isRepoBaseRepo(params), + getLatestBaseRepoCommitSha(), + getGitHubCommitsBehind(params), + ]); + + const isBehind = behindBy > 0; + const label = isBehind ? `${behindBy} Behind` : 'Synced'; + const title = isBehind + // eslint-disable-next-line max-len + ? `This fork is ${behindBy} commit${behindBy === 1 ? '' : 's'} behind. Consider syncing on GitHub for the latest updates.` + : 'This fork is up to date.'; + + return { + url, + isForkedFromBase, + isBaseRepo, + latestBaseRepoCommitSha, + behindBy, + isBehind, + label, + title, + }; +}; diff --git a/src/app/admin/configuration/page.tsx b/src/app/admin/configuration/page.tsx index ce4719f3..ecb28453 100644 --- a/src/app/admin/configuration/page.tsx +++ b/src/app/admin/configuration/page.tsx @@ -1,6 +1,8 @@ import ClearCacheButton from '@/admin/ClearCacheButton'; +import GitHubForkStatusBadge from '@/admin/github/GitHubForkStatusBadge'; import Container from '@/components/Container'; import SiteGrid from '@/components/SiteGrid'; +import { IS_DEVELOPMENT, IS_VERCEL_GIT_PROVIDER_GITHUB } from '@/site/config'; import SiteChecklist from '@/site/SiteChecklist'; export default async function AdminConfigurationPage() { @@ -8,10 +10,12 @@ export default async function AdminConfigurationPage() { -
-
+
+
App Configuration
+ {(IS_VERCEL_GIT_PROVIDER_GITHUB || IS_DEVELOPMENT) && + }
diff --git a/src/site/SiteChecklist.tsx b/src/site/SiteChecklist.tsx index 1b7dab75..935d0f9c 100644 --- a/src/site/SiteChecklist.tsx +++ b/src/site/SiteChecklist.tsx @@ -11,7 +11,7 @@ export default function SiteChecklist({ return ( }> diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 38b8ec37..a716f69d 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -94,17 +94,13 @@ export default function SiteChecklistClient({ storageError, kvError, aiError, - // Git Meta - isForkedFromBaseRepo, // Component props simplifiedView, - isTestingConnections, + isAnalyzingConfiguration, }: ConfigChecklistStatus & Partial>> & { simplifiedView?: boolean - isTestingConnections?: boolean -} & { - isForkedFromBaseRepo?: boolean + isAnalyzingConfiguration?: boolean }) { const renderLink = (href: string, text: string, external = true) => <> @@ -216,11 +212,11 @@ export default function SiteChecklistClient({ icon={} > {databaseError && renderError({ connection: { provider: 'Database', error: databaseError}, @@ -247,7 +243,7 @@ export default function SiteChecklistClient({ {storageError && renderError({ connection: { provider: 'Storage', error: storageError}, @@ -302,11 +298,11 @@ export default function SiteChecklistClient({ icon={} > Store auth secret in environment variable: {!hasAuthSecret && @@ -378,11 +374,11 @@ export default function SiteChecklistClient({ optional > {aiError && renderError({ @@ -394,11 +390,11 @@ export default function SiteChecklistClient({ {renderEnvVars(['OPENAI_SECRET_KEY'])} {kvError && renderError({ @@ -669,18 +665,13 @@ export default function SiteChecklistClient({    {commitSha ? commitUrl - ? <> - - {commitSha} - -   - {isForkedFromBaseRepo && - Forked} - + ? + {commitSha} + : {commitSha} : 'Not Found'}
diff --git a/src/site/SiteChecklistServer.tsx b/src/site/SiteChecklistServer.tsx index 160be58d..e50255ab 100644 --- a/src/site/SiteChecklistServer.tsx +++ b/src/site/SiteChecklistServer.tsx @@ -1,11 +1,6 @@ import SiteChecklistClient from './SiteChecklistClient'; -import { - CONFIG_CHECKLIST_STATUS, - VERCEL_GIT_REPO_OWNER, - VERCEL_GIT_REPO_SLUG, -} from '@/site/config'; +import { CONFIG_CHECKLIST_STATUS } from '@/site/config'; import { testConnectionsAction } from '@/admin/actions'; -import { isRepoForkedFromBase } from '@/utility/github'; export default async function SiteChecklistServer({ simplifiedView, @@ -13,16 +8,11 @@ export default async function SiteChecklistServer({ simplifiedView?: boolean }) { const connectionErrors = await testConnectionsAction().catch(() => ({})); - const isForkedFromBaseRepo = await isRepoForkedFromBase( - VERCEL_GIT_REPO_OWNER, - VERCEL_GIT_REPO_SLUG, - ); return ( ); diff --git a/src/site/config.ts b/src/site/config.ts index f47ff042..4a16fe42 100644 --- a/src/site/config.ts +++ b/src/site/config.ts @@ -27,19 +27,19 @@ export const VERCEL_GIT_COMMIT_SHA = export const VERCEL_GIT_COMMIT_SHA_SHORT = VERCEL_GIT_COMMIT_SHA ? VERCEL_GIT_COMMIT_SHA.slice(0, 7) : undefined; -export const VERCEL_IS_PROVIDER_GITHUB = VERCEL_GIT_PROVIDER === 'github'; -export const VERCEL_GIT_COMMIT_URL = VERCEL_IS_PROVIDER_GITHUB +export const IS_VERCEL_GIT_PROVIDER_GITHUB = VERCEL_GIT_PROVIDER === 'github'; +export const VERCEL_GIT_COMMIT_URL = IS_VERCEL_GIT_PROVIDER_GITHUB // eslint-disable-next-line max-len ? `https://github.com/${VERCEL_GIT_REPO_OWNER}/${VERCEL_GIT_REPO_SLUG}/commit/${VERCEL_GIT_COMMIT_SHA}` : undefined; -const VERCEL_ENV = process.env.NEXT_PUBLIC_VERCEL_ENV; -const VERCEL_PRODUCTION_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL; -const VERCEL_DEPLOYMENT_URL = process.env.NEXT_PUBLIC_VERCEL_URL; -const VERCEL_BRANCH_URL = process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL; -const VERCEL_BRANCH = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF; +export const VERCEL_ENV = process.env.NEXT_PUBLIC_VERCEL_ENV; +export const VERCEL_PRODUCTION_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL; +export const VERCEL_DEPLOYMENT_URL = process.env.NEXT_PUBLIC_VERCEL_URL; +export const VERCEL_BRANCH_URL = process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL; +export const VERCEL_BRANCH = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF; // Last resort: cannot be used reliably -const VERCEL_PROJECT_URL = VERCEL_BRANCH_URL && VERCEL_BRANCH +export const VERCEL_PROJECT_URL = VERCEL_BRANCH_URL && VERCEL_BRANCH ? `${VERCEL_BRANCH_URL.split(`-git-${VERCEL_BRANCH}-`)[0]}.vercel.app` : undefined; @@ -49,6 +49,7 @@ export const IS_PRODUCTION = process.env.NODE_ENV === 'production' && ( !VERCEL_ENV ); +export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; export const IS_PREVIEW = VERCEL_ENV === 'preview'; export const VERCEL_BYPASS_KEY = 'x-vercel-protection-bypass'; diff --git a/src/site/globals.css b/src/site/globals.css index 999cf3c5..a949e023 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -155,6 +155,9 @@ text-red-500 dark:text-red-400 } /* Utilities: Border */ + .border-main { + @apply border-gray-200 dark:border-gray-700 + } .border-subtle { @apply border border-gray-200 dark:border-gray-800 diff --git a/src/utility/github.ts b/src/utility/github.ts deleted file mode 100644 index 53201909..00000000 --- a/src/utility/github.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { VERCEL_IS_PROVIDER_GITHUB } from '@/site/config'; - -const BASE_OWNER = 'sambecker'; -const BASE_REPO = 'exif-photo-blog'; - -type RepoParams = Parameters<(owner?: string, repo?: string) => unknown>; - -const getRepoUrl = (owner = BASE_OWNER, repo = BASE_REPO) => - `https://api.github.com/repos/${owner}/${repo}`; - -const getCommitsUrl = (...args: RepoParams) => - `${getRepoUrl(...args)}/commits/main`; - -export const fetchLatestBaseRepoCommitSha = async () => { - if (VERCEL_IS_PROVIDER_GITHUB) { - const response = await fetch(getCommitsUrl()); - const data = await response.json(); - return data.sha.slice(0, 7); - } else { - return undefined; - } -}; - -export const isRepoForkedFromBase = async (...args: RepoParams) => { - if (VERCEL_IS_PROVIDER_GITHUB) { - const response = await fetch(getRepoUrl(...args)); - const data = await response.json(); - return data.fork && data.source?.full_name === `${BASE_OWNER}/${BASE_REPO}`; - } else { - return false; - } -};