Add git sync status for forked repos
This commit is contained in:
parent
843c7046b2
commit
10c7ba4240
14
src/admin/github/GitHubForkStatusBadge.tsx
Normal file
14
src/admin/github/GitHubForkStatusBadge.tsx
Normal file
@ -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
|
||||
? <GitHubForkStatusBadgeClient label="Local" />
|
||||
: IS_VERCEL_GIT_PROVIDER_GITHUB
|
||||
? <Suspense>
|
||||
<GitHubForkStatusBadgeServer />
|
||||
</Suspense>
|
||||
: null;
|
||||
}
|
||||
74
src/admin/github/GitHubForkStatusBadgeClient.tsx
Normal file
74
src/admin/github/GitHubForkStatusBadgeClient.tsx
Normal file
@ -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
|
||||
? <Spinner
|
||||
color="text"
|
||||
className="translate-x-[3px]"
|
||||
/>
|
||||
: <BiLogoGithub size={17} />}
|
||||
{label ?? 'Checking'}
|
||||
</>;
|
||||
|
||||
return url
|
||||
? <Link
|
||||
target="_blank"
|
||||
href={url}
|
||||
title={title}
|
||||
className={className}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
: <span
|
||||
title={title}
|
||||
className={className}
|
||||
>
|
||||
{content}
|
||||
</span>;
|
||||
}
|
||||
27
src/admin/github/GitHubForkStatusBadgeServer.tsx
Normal file
27
src/admin/github/GitHubForkStatusBadgeServer.tsx
Normal file
@ -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 (
|
||||
<GitHubForkStatusBadgeClient {...{
|
||||
url,
|
||||
label,
|
||||
title,
|
||||
style: isBehind ? 'warning' : 'mono',
|
||||
}} />
|
||||
);
|
||||
}
|
||||
90
src/admin/github/index.ts
Normal file
90
src/admin/github/index.ts
Normal file
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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() {
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-grow">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="grow">
|
||||
App Configuration
|
||||
</div>
|
||||
{(IS_VERCEL_GIT_PROVIDER_GITHUB || IS_DEVELOPMENT) &&
|
||||
<GitHubForkStatusBadge />}
|
||||
<ClearCacheButton />
|
||||
</div>
|
||||
<Container spaceChildren={false}>
|
||||
|
||||
@ -11,7 +11,7 @@ export default function SiteChecklist({
|
||||
return (
|
||||
<Suspense fallback={<SiteChecklistClient {...{
|
||||
...CONFIG_CHECKLIST_STATUS,
|
||||
isTestingConnections: true,
|
||||
isAnalyzingConfiguration: true,
|
||||
simplifiedView,
|
||||
}} /> }>
|
||||
<SiteChecklistServer {...{ simplifiedView }} />
|
||||
|
||||
@ -94,17 +94,13 @@ export default function SiteChecklistClient({
|
||||
storageError,
|
||||
kvError,
|
||||
aiError,
|
||||
// Git Meta
|
||||
isForkedFromBaseRepo,
|
||||
// Component props
|
||||
simplifiedView,
|
||||
isTestingConnections,
|
||||
isAnalyzingConfiguration,
|
||||
}: ConfigChecklistStatus &
|
||||
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
|
||||
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={<BiData size={16} />}
|
||||
>
|
||||
<ChecklistRow
|
||||
title={hasDatabase && isTestingConnections
|
||||
title={hasDatabase && isAnalyzingConfiguration
|
||||
? 'Testing database connection'
|
||||
: 'Setup database'}
|
||||
status={hasDatabase}
|
||||
isPending={hasDatabase && isTestingConnections}
|
||||
isPending={hasDatabase && isAnalyzingConfiguration}
|
||||
>
|
||||
{databaseError && renderError({
|
||||
connection: { provider: 'Database', error: databaseError},
|
||||
@ -247,7 +243,7 @@ export default function SiteChecklistClient({
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title={
|
||||
hasStorageProvider && isTestingConnections
|
||||
hasStorageProvider && isAnalyzingConfiguration
|
||||
? 'Testing storage connection'
|
||||
: !hasStorageProvider
|
||||
? 'Setup storage (one of the following)'
|
||||
@ -256,7 +252,7 @@ export default function SiteChecklistClient({
|
||||
? `Setup storage (new uploads go to: ${labelForStorage(currentStorage)})`
|
||||
: 'Setup storage'}
|
||||
status={hasStorageProvider}
|
||||
isPending={hasStorageProvider && isTestingConnections}
|
||||
isPending={hasStorageProvider && isAnalyzingConfiguration}
|
||||
>
|
||||
{storageError && renderError({
|
||||
connection: { provider: 'Storage', error: storageError},
|
||||
@ -302,11 +298,11 @@ export default function SiteChecklistClient({
|
||||
icon={<BiLockAlt size={16} />}
|
||||
>
|
||||
<ChecklistRow
|
||||
title={!hasAuthSecret && isTestingConnections
|
||||
title={!hasAuthSecret && isAnalyzingConfiguration
|
||||
? 'Generating secret'
|
||||
: 'Setup auth'}
|
||||
status={hasAuthSecret}
|
||||
isPending={!hasAuthSecret && isTestingConnections}
|
||||
isPending={!hasAuthSecret && isAnalyzingConfiguration}
|
||||
>
|
||||
Store auth secret in environment variable:
|
||||
{!hasAuthSecret &&
|
||||
@ -378,11 +374,11 @@ export default function SiteChecklistClient({
|
||||
optional
|
||||
>
|
||||
<ChecklistRow
|
||||
title={isAiTextGenerationEnabled && isTestingConnections
|
||||
title={isAiTextGenerationEnabled && isAnalyzingConfiguration
|
||||
? 'Testing OpenAI connection'
|
||||
: 'Add OpenAI secret key'}
|
||||
status={isAiTextGenerationEnabled}
|
||||
isPending={isAiTextGenerationEnabled && isTestingConnections}
|
||||
isPending={isAiTextGenerationEnabled && isAnalyzingConfiguration}
|
||||
optional
|
||||
>
|
||||
{aiError && renderError({
|
||||
@ -394,11 +390,11 @@ export default function SiteChecklistClient({
|
||||
{renderEnvVars(['OPENAI_SECRET_KEY'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title={hasVercelKv && isTestingConnections
|
||||
title={hasVercelKv && isAnalyzingConfiguration
|
||||
? 'Testing KV connection'
|
||||
: 'Enable rate limiting'}
|
||||
status={hasVercelKv}
|
||||
isPending={hasVercelKv && isTestingConnections}
|
||||
isPending={hasVercelKv && isAnalyzingConfiguration}
|
||||
optional
|
||||
>
|
||||
{kvError && renderError({
|
||||
@ -669,18 +665,13 @@ export default function SiteChecklistClient({
|
||||
|
||||
{commitSha
|
||||
? commitUrl
|
||||
? <>
|
||||
<Link
|
||||
title={commitMessage}
|
||||
href={commitUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{commitSha}
|
||||
</Link>
|
||||
|
||||
{isForkedFromBaseRepo &&
|
||||
<span className="text-dim">Forked</span>}
|
||||
</>
|
||||
? <Link
|
||||
title={commitMessage}
|
||||
href={commitUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{commitSha}
|
||||
</Link>
|
||||
: <span title={commitMessage}>{commitSha}</span>
|
||||
: 'Not Found'}
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<SiteChecklistClient {...{
|
||||
...CONFIG_CHECKLIST_STATUS,
|
||||
...connectionErrors,
|
||||
isForkedFromBaseRepo,
|
||||
simplifiedView,
|
||||
}} />
|
||||
);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user