mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
feat(changelog): added changelog and gh action to auto-release (#1423)
* feat(changelog): added changelog * finished changelog * updated metadata * reverted env * cleanup --------- Co-authored-by: waleed <waleed>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const DEFAULT_STARS = '15k'
|
||||
const DEFAULT_STARS = '15.4k'
|
||||
|
||||
const logger = createLogger('GitHubStars')
|
||||
|
||||
|
||||
@@ -214,6 +214,12 @@ export default function Footer({ fullWidth = false }: FooterProps) {
|
||||
>
|
||||
Enterprise
|
||||
</Link>
|
||||
<Link
|
||||
href='/changelog'
|
||||
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
>
|
||||
Changelog
|
||||
</Link>
|
||||
<Link
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
|
||||
@@ -20,7 +20,7 @@ interface NavProps {
|
||||
}
|
||||
|
||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||
const [githubStars, setGithubStars] = useState('15k')
|
||||
const [githubStars, setGithubStars] = useState('15.4k')
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
68
apps/sim/app/changelog.xml/route.ts
Normal file
68
apps/sim/app/changelog.xml/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
export const revalidate = 3600
|
||||
|
||||
interface Release {
|
||||
id: number
|
||||
tag_name: string
|
||||
name: string
|
||||
body: string
|
||||
html_url: string
|
||||
published_at: string
|
||||
prerelease: boolean
|
||||
}
|
||||
|
||||
function escapeXml(str: string) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/simstudioai/sim/releases', {
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
next: { revalidate },
|
||||
})
|
||||
const releases: Release[] = await res.json()
|
||||
const items = (releases || [])
|
||||
.filter((r) => !r.prerelease)
|
||||
.map(
|
||||
(r) => `
|
||||
<item>
|
||||
<title>${escapeXml(r.name || r.tag_name)}</title>
|
||||
<link>${r.html_url}</link>
|
||||
<guid isPermaLink="true">${r.html_url}</guid>
|
||||
<pubDate>${new Date(r.published_at).toUTCString()}</pubDate>
|
||||
<description><![CDATA[${r.body || ''}]]></description>
|
||||
</item>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Sim Changelog</title>
|
||||
<link>https://sim.dev/changelog</link>
|
||||
<description>Latest changes, fixes and updates in Sim.</description>
|
||||
<language>en-us</language>
|
||||
${items}
|
||||
</channel>
|
||||
</rss>`
|
||||
|
||||
return new NextResponse(xml, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||
'Cache-Control': `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate}`,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
return new NextResponse('Service Unavailable', { status: 503 })
|
||||
}
|
||||
}
|
||||
105
apps/sim/app/changelog/components/changelog-content.tsx
Normal file
105
apps/sim/app/changelog/components/changelog-content.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { BookOpen, Github, Rss } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { inter } from '@/app/fonts/inter'
|
||||
import { soehne } from '@/app/fonts/soehne/soehne'
|
||||
import ChangelogList from './timeline-list'
|
||||
|
||||
export interface ChangelogEntry {
|
||||
tag: string
|
||||
title: string
|
||||
content: string
|
||||
date: string
|
||||
url: string
|
||||
contributors?: string[]
|
||||
}
|
||||
|
||||
function extractMentions(body: string): string[] {
|
||||
const matches = body.match(/@([A-Za-z0-9-]+)/g) ?? []
|
||||
const uniq = Array.from(new Set(matches.map((m) => m.slice(1))))
|
||||
return uniq
|
||||
}
|
||||
|
||||
export default async function ChangelogContent() {
|
||||
let entries: ChangelogEntry[] = []
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
'https://api.github.com/repos/simstudioai/sim/releases?per_page=10&page=1',
|
||||
{
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
next: { revalidate: 3600 },
|
||||
}
|
||||
)
|
||||
const releases: any[] = await res.json()
|
||||
entries = (releases || [])
|
||||
.filter((r) => !r.prerelease)
|
||||
.map((r) => ({
|
||||
tag: r.tag_name,
|
||||
title: r.name || r.tag_name,
|
||||
content: String(r.body || ''),
|
||||
date: r.published_at,
|
||||
url: r.html_url,
|
||||
contributors: extractMentions(String(r.body || '')),
|
||||
}))
|
||||
} catch (err) {
|
||||
entries = []
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-background'>
|
||||
<div className='relative grid md:grid-cols-2'>
|
||||
{/* Left intro panel */}
|
||||
<div className='relative top-0 overflow-hidden border-border border-b px-6 py-16 sm:px-10 md:sticky md:h-dvh md:border-r md:border-b-0 md:px-12 md:py-24'>
|
||||
<div className='absolute inset-0 bg-grid-pattern opacity-[0.03] dark:opacity-[0.06]' />
|
||||
<div className='absolute inset-0 bg-gradient-to-tr from-background via-transparent to-background/60' />
|
||||
|
||||
<div className='relative mx-auto h-full max-w-xl md:flex md:flex-col md:justify-center'>
|
||||
<h1
|
||||
className={`${soehne.className} mt-6 font-semibold text-4xl tracking-tight sm:text-5xl`}
|
||||
>
|
||||
Changelog
|
||||
</h1>
|
||||
<p className={`${inter.className} mt-4 text-muted-foreground text-sm`}>
|
||||
Stay up-to-date with the latest features, improvements, and bug fixes in Sim. All
|
||||
changes are documented here with detailed release notes.
|
||||
</p>
|
||||
<hr className='mt-6 border-border' />
|
||||
|
||||
<div className='mt-6 flex flex-wrap items-center gap-3 text-sm'>
|
||||
<Link
|
||||
href='https://github.com/simstudioai/sim/releases'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group inline-flex items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[14px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all sm:text-[16px]'
|
||||
>
|
||||
<Github className='h-4 w-4' />
|
||||
View on GitHub
|
||||
</Link>
|
||||
<Link
|
||||
href='https://docs.sim.ai'
|
||||
className='inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 hover:bg-muted'
|
||||
>
|
||||
<BookOpen className='h-4 w-4' />
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href='/changelog.xml'
|
||||
className='inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 hover:bg-muted'
|
||||
>
|
||||
<Rss className='h-4 w-4' />
|
||||
RSS Feed
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right timeline */}
|
||||
<div className='relative px-4 py-10 sm:px-6 md:px-8 md:py-12'>
|
||||
<div className='relative max-w-2xl pl-8'>
|
||||
<ChangelogList initialEntries={entries} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
224
apps/sim/app/changelog/components/timeline-list.tsx
Normal file
224
apps/sim/app/changelog/components/timeline-list.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { inter } from '@/app/fonts/inter'
|
||||
import { soehne } from '@/app/fonts/soehne/soehne'
|
||||
import type { ChangelogEntry } from './changelog-content'
|
||||
|
||||
type Props = { initialEntries: ChangelogEntry[] }
|
||||
|
||||
function sanitizeContent(body: string): string {
|
||||
return body.replace(/ /g, '')
|
||||
}
|
||||
|
||||
function stripContributors(body: string): string {
|
||||
let output = body
|
||||
output = output.replace(
|
||||
/(^|\n)#{1,6}\s*Contributors\s*\n[\s\S]*?(?=\n\s*\n|\n#{1,6}\s|$)/gi,
|
||||
'\n'
|
||||
)
|
||||
output = output.replace(
|
||||
/(^|\n)\s*(?:\*\*|__)?\s*Contributors\s*(?:\*\*|__)?\s*:?\s*\n[\s\S]*?(?=\n\s*\n|\n#{1,6}\s|$)/gi,
|
||||
'\n'
|
||||
)
|
||||
output = output.replace(
|
||||
/(^|\n)[-*+]\s*(?:@[A-Za-z0-9-]+(?:\s*,\s*|\s+))+@[A-Za-z0-9-]+\s*(?=\n)/g,
|
||||
'\n'
|
||||
)
|
||||
output = output.replace(
|
||||
/(^|\n)\s*(?:@[A-Za-z0-9-]+(?:\s*,\s*|\s+))+@[A-Za-z0-9-]+\s*(?=\n)/g,
|
||||
'\n'
|
||||
)
|
||||
return output
|
||||
}
|
||||
|
||||
function isContributorsLabel(nodeChildren: React.ReactNode): boolean {
|
||||
return /^\s*contributors\s*:?\s*$/i.test(String(nodeChildren))
|
||||
}
|
||||
|
||||
function stripPrReferences(body: string): string {
|
||||
return body.replace(/\s*\(\s*\[#\d+\]\([^)]*\)\s*\)/g, '').replace(/\s*\(\s*#\d+\s*\)/g, '')
|
||||
}
|
||||
|
||||
function cleanMarkdown(body: string): string {
|
||||
const sanitized = sanitizeContent(body)
|
||||
const withoutContribs = stripContributors(sanitized)
|
||||
const withoutPrs = stripPrReferences(withoutContribs)
|
||||
return withoutPrs
|
||||
}
|
||||
|
||||
function extractMentions(body: string): string[] {
|
||||
const matches = body.match(/@([A-Za-z0-9-]+)/g) ?? []
|
||||
return Array.from(new Set(matches.map((m) => m.slice(1))))
|
||||
}
|
||||
|
||||
export default function ChangelogList({ initialEntries }: Props) {
|
||||
const [entries, setEntries] = React.useState<ChangelogEntry[]>(initialEntries)
|
||||
const [page, setPage] = React.useState<number>(1)
|
||||
const [loading, setLoading] = React.useState<boolean>(false)
|
||||
const [done, setDone] = React.useState<boolean>(false)
|
||||
|
||||
const loadMore = async () => {
|
||||
if (loading || done) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const nextPage = page + 1
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/simstudioai/sim/releases?per_page=10&page=${nextPage}`,
|
||||
{ headers: { Accept: 'application/vnd.github+json' } }
|
||||
)
|
||||
const releases: any[] = await res.json()
|
||||
const mapped: ChangelogEntry[] = (releases || [])
|
||||
.filter((r) => !r.prerelease)
|
||||
.map((r) => ({
|
||||
tag: r.tag_name,
|
||||
title: r.name || r.tag_name,
|
||||
content: sanitizeContent(String(r.body || '')),
|
||||
date: r.published_at,
|
||||
url: r.html_url,
|
||||
contributors: extractMentions(String(r.body || '')),
|
||||
}))
|
||||
|
||||
if (mapped.length === 0) {
|
||||
setDone(true)
|
||||
} else {
|
||||
setEntries((prev) => [...prev, ...mapped])
|
||||
setPage(nextPage)
|
||||
}
|
||||
} catch {
|
||||
setDone(true)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-10'>
|
||||
{entries.map((entry) => (
|
||||
<div key={entry.tag}>
|
||||
<div className='flex items-baseline justify-between gap-4'>
|
||||
<div className={`${soehne.className} font-semibold text-[18px] tracking-tight`}>
|
||||
{entry.tag}
|
||||
</div>
|
||||
<div className={`${inter.className} text-muted-foreground text-xs`}>
|
||||
{new Date(entry.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${inter.className} prose prose-sm dark:prose-invert max-w-none prose-headings:font-semibold prose-a:text-brand-primary prose-headings:text-foreground prose-p:text-muted-foreground prose-a:no-underline hover:prose-a:underline`}
|
||||
>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h2: ({ children, ...props }) =>
|
||||
isContributorsLabel(children) ? null : (
|
||||
<h3
|
||||
className={`${soehne.className} mt-5 mb-2 font-medium text-[13px] text-foreground tracking-tight`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h3: ({ children, ...props }) =>
|
||||
isContributorsLabel(children) ? null : (
|
||||
<h4
|
||||
className={`${soehne.className} mt-4 mb-1 font-medium text-[13px] text-foreground tracking-tight`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
ul: ({ children, ...props }) => (
|
||||
<ul className='mt-2 mb-3 space-y-1.5' {...props}>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
li: ({ children, ...props }) => {
|
||||
const text = String(children)
|
||||
if (/^\s*contributors\s*:?\s*$/i.test(text)) return null
|
||||
return (
|
||||
<li className='text-[13px] text-muted-foreground leading-relaxed' {...props}>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
},
|
||||
p: ({ children, ...props }) =>
|
||||
/^\s*contributors\s*:?\s*$/i.test(String(children)) ? null : (
|
||||
<p
|
||||
className='mb-3 text-[13px] text-muted-foreground leading-relaxed'
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
strong: ({ children, ...props }) => (
|
||||
<strong className='font-medium text-foreground' {...props}>
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
code: ({ children, ...props }) => (
|
||||
<code
|
||||
className='rounded bg-muted px-1 py-0.5 font-mono text-foreground text-xs'
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
img: () => null,
|
||||
a: ({ className, ...props }: any) => (
|
||||
<a
|
||||
{...props}
|
||||
className={`underline ${className ?? ''}`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{cleanMarkdown(entry.content)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
{entry.contributors && entry.contributors.length > 0 && (
|
||||
<div className='-space-x-2 mt-3 flex'>
|
||||
{entry.contributors.slice(0, 5).map((contributor) => (
|
||||
<Avatar key={contributor} className='size-6 ring-2 ring-background'>
|
||||
<AvatarImage
|
||||
src={`https://avatars.githubusercontent.com/${contributor}`}
|
||||
alt={`@${contributor}`}
|
||||
className='hover:z-10'
|
||||
/>
|
||||
<AvatarFallback>{contributor.slice(0, 2).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
{entry.contributors.length > 5 && (
|
||||
<div className='relative flex size-6 items-center justify-center rounded-full bg-muted text-[10px] text-foreground ring-2 ring-background hover:z-10'>
|
||||
+{entry.contributors.length - 5}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!done && (
|
||||
<div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={loadMore}
|
||||
disabled={loading}
|
||||
className='rounded-md border border-border px-3 py-1.5 text-[13px] hover:bg-muted disabled:opacity-60'
|
||||
>
|
||||
{loading ? 'Loading…' : 'Show more'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
10
apps/sim/app/changelog/layout.tsx
Normal file
10
apps/sim/app/changelog/layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
|
||||
export default function ChangelogLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className='min-h-screen bg-background font-geist-sans text-foreground'>
|
||||
<Nav />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
apps/sim/app/changelog/page.tsx
Normal file
16
apps/sim/app/changelog/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from 'next'
|
||||
import ChangelogContent from './components/changelog-content'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Changelog',
|
||||
description: 'Stay up-to-date with the latest features, improvements, and bug fixes in Sim.',
|
||||
openGraph: {
|
||||
title: 'Changelog',
|
||||
description: 'Stay up-to-date with the latest features, improvements, and bug fixes in Sim.',
|
||||
type: 'website',
|
||||
},
|
||||
}
|
||||
|
||||
export default function ChangelogPage() {
|
||||
return <ChangelogContent />
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
export function ConditionalThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
// Force light mode for landing page (root path and /homepage), auth verify, invite, and legal pages
|
||||
// Force light mode for certain pages
|
||||
const forcedTheme =
|
||||
pathname === '/' ||
|
||||
pathname === '/homepage' ||
|
||||
@@ -16,7 +16,8 @@ export function ConditionalThemeProvider({ children, ...props }: ThemeProviderPr
|
||||
pathname.startsWith('/terms') ||
|
||||
pathname.startsWith('/privacy') ||
|
||||
pathname.startsWith('/invite') ||
|
||||
pathname.startsWith('/verify')
|
||||
pathname.startsWith('/verify') ||
|
||||
pathname.startsWith('/changelog')
|
||||
? 'light'
|
||||
: undefined
|
||||
|
||||
|
||||
@@ -2,8 +2,26 @@
|
||||
|
||||
import * as React from 'react'
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const avatarStatusVariants = cva(
|
||||
'flex items-center rounded-full size-2 border-2 border-background',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
online: 'bg-green-600',
|
||||
offline: 'bg-zinc-600 dark:bg-zinc-300',
|
||||
busy: 'bg-yellow-600',
|
||||
away: 'bg-blue-600',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'online',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
@@ -22,7 +40,7 @@ const AvatarImage = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn('aspect-square h-full w-full', className)}
|
||||
className={cn('aspect-square h-full w-full object-cover object-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
@@ -35,7 +53,7 @@ const AvatarFallback = React.forwardRef<
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center rounded-full bg-muted',
|
||||
'flex h-full w-full items-center justify-center rounded-full border border-border bg-accent text-accent-foreground text-xs',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -43,4 +61,28 @@ const AvatarFallback = React.forwardRef<
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
function AvatarIndicator({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='avatar-indicator'
|
||||
className={cn('absolute flex size-6 items-center justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarStatus({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof avatarStatusVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='avatar-status'
|
||||
className={cn(avatarStatusVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback, AvatarIndicator, AvatarStatus, avatarStatusVariants }
|
||||
|
||||
@@ -62,6 +62,7 @@ export const buildTimeCSPDirectives: CSPDirectives = {
|
||||
'https://*.public.blob.vercel-storage.com',
|
||||
'https://*.s3.amazonaws.com',
|
||||
'https://s3.amazonaws.com',
|
||||
'https://github.com/*',
|
||||
...(env.S3_BUCKET_NAME && env.AWS_REGION
|
||||
? [`https://${env.S3_BUCKET_NAME}.s3.${env.AWS_REGION}.amazonaws.com`]
|
||||
: []),
|
||||
@@ -73,6 +74,7 @@ export const buildTimeCSPDirectives: CSPDirectives = {
|
||||
: []),
|
||||
'https://*.amazonaws.com',
|
||||
'https://*.blob.core.windows.net',
|
||||
'https://github.com/*',
|
||||
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL),
|
||||
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_FAVICON_URL),
|
||||
],
|
||||
@@ -107,6 +109,8 @@ export const buildTimeCSPDirectives: CSPDirectives = {
|
||||
'https://*.vercel.app',
|
||||
'wss://*.vercel.app',
|
||||
'https://pro.ip-api.com',
|
||||
'https://api.github.com',
|
||||
'https://github.com/*',
|
||||
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL),
|
||||
...getHostnameFromUrl(env.NEXT_PUBLIC_PRIVACY_URL),
|
||||
...getHostnameFromUrl(env.NEXT_PUBLIC_TERMS_URL),
|
||||
@@ -169,7 +173,7 @@ export function generateRuntimeCSP(): string {
|
||||
img-src 'self' data: blob: https://*.googleusercontent.com https://*.google.com https://*.atlassian.com https://cdn.discordapp.com https://*.githubusercontent.com https://*.public.blob.vercel-storage.com ${brandLogoDomain} ${brandFaviconDomain};
|
||||
media-src 'self' blob:;
|
||||
font-src 'self' https://fonts.gstatic.com;
|
||||
connect-src 'self' ${appUrl} ${ollamaUrl} ${socketUrl} ${socketWsUrl} https://*.up.railway.app wss://*.up.railway.app https://api.browser-use.com https://api.exa.ai https://api.firecrawl.dev https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.blob.core.windows.net https://*.vercel-insights.com https://vitals.vercel-insights.com https://*.atlassian.com https://*.supabase.co https://vercel.live https://*.vercel.live https://vercel.com https://*.vercel.app wss://*.vercel.app https://pro.ip-api.com ${dynamicDomainsStr};
|
||||
connect-src 'self' ${appUrl} ${ollamaUrl} ${socketUrl} ${socketWsUrl} https://*.up.railway.app wss://*.up.railway.app https://api.browser-use.com https://api.exa.ai https://api.firecrawl.dev https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.blob.core.windows.net https://api.github.com https://github.com/* https://*.vercel-insights.com https://vitals.vercel-insights.com https://*.atlassian.com https://*.supabase.co https://vercel.live https://*.vercel.live https://vercel.com https://*.vercel.app wss://*.vercel.app https://pro.ip-api.com ${dynamicDomainsStr};
|
||||
frame-src https://drive.google.com https://docs.google.com https://*.google.com;
|
||||
frame-ancestors 'self';
|
||||
form-action 'self';
|
||||
|
||||
39
bun.lock
39
bun.lock
@@ -20,6 +20,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0-beta.5",
|
||||
"@next/env": "15.4.1",
|
||||
"@octokit/rest": "^21.0.0",
|
||||
"@types/bcryptjs": "3.0.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"husky": "9.1.7",
|
||||
@@ -744,6 +745,30 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
|
||||
|
||||
"@octokit/core": ["@octokit/core@6.1.6", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA=="],
|
||||
|
||||
"@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
|
||||
|
||||
"@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
|
||||
|
||||
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@11.6.0", "", { "dependencies": { "@octokit/types": "^13.10.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw=="],
|
||||
|
||||
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@5.3.1", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@13.5.0", "", { "dependencies": { "@octokit/types": "^13.10.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw=="],
|
||||
|
||||
"@octokit/request": ["@octokit/request@9.2.4", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA=="],
|
||||
|
||||
"@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
|
||||
|
||||
"@octokit/rest": ["@octokit/rest@21.1.1", "", { "dependencies": { "@octokit/core": "^6.1.4", "@octokit/plugin-paginate-rest": "^11.4.2", "@octokit/plugin-request-log": "^5.3.1", "@octokit/plugin-rest-endpoint-methods": "^13.3.0" } }, "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg=="],
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="],
|
||||
@@ -1672,6 +1697,8 @@
|
||||
|
||||
"bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="],
|
||||
|
||||
"before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
|
||||
|
||||
"better-auth": ["better-auth@1.2.9", "", { "dependencies": { "@better-auth/utils": "0.2.5", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.8", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.28.2", "nanostores": "^0.11.3", "zod": "^3.24.1" } }, "sha512-WLqBXDzuaCQetQctLGC5oTfGmL32zUvxnM4Y+LZkhwseMaZWq5EKI+c/ZATgz2YkFt7726q659PF8CfB9P1VuA=="],
|
||||
|
||||
"better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="],
|
||||
@@ -2090,6 +2117,8 @@
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
|
||||
|
||||
"fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
@@ -3346,6 +3375,8 @@
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
|
||||
|
||||
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"unplugin": ["unplugin@1.0.1", "", { "dependencies": { "acorn": "^8.8.1", "chokidar": "^3.5.3", "webpack-sources": "^3.2.3", "webpack-virtual-modules": "^0.5.0" } }, "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA=="],
|
||||
@@ -3524,6 +3555,10 @@
|
||||
|
||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@0.25.0", "", {}, "sha512-V3N+MDBiv0TUlorbgiSqk6CvcP876CYUk/41Tg6s8OIyvniTwprE6vPvFQayuABiVkGlHOxv1Mlvp0w4qNdnVg=="],
|
||||
|
||||
"@opentelemetry/exporter-collector/@opentelemetry/resources": ["@opentelemetry/resources@0.25.0", "", { "dependencies": { "@opentelemetry/core": "0.25.0", "@opentelemetry/semantic-conventions": "0.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.2" } }, "sha512-O46u53vDBlxCML8O9dIjsRcCC2VT5ri1upwhp02ITobgJ16aVD/iScCo1lPl/x2E7yq9uwzMINENiiYZRFb6XA=="],
|
||||
@@ -4218,6 +4253,10 @@
|
||||
|
||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@opentelemetry/exporter-collector/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@0.25.0", "", {}, "sha512-V3N+MDBiv0TUlorbgiSqk6CvcP876CYUk/41Tg6s8OIyvniTwprE6vPvFQayuABiVkGlHOxv1Mlvp0w4qNdnVg=="],
|
||||
|
||||
"@opentelemetry/instrumentation-amqplib/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"check": "bunx biome check --files-ignore-unknown=true",
|
||||
"prepare": "bun husky",
|
||||
"prebuild": "bun run lint:check",
|
||||
"type-check": "turbo run type-check"
|
||||
"type-check": "turbo run type-check",
|
||||
"release": "bun run scripts/create-single-release.ts"
|
||||
},
|
||||
"overrides": {
|
||||
"react": "19.1.0",
|
||||
@@ -49,6 +50,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0-beta.5",
|
||||
"@octokit/rest": "^21.0.0",
|
||||
"@next/env": "15.4.1",
|
||||
"@types/bcryptjs": "3.0.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
|
||||
385
scripts/create-single-release.ts
Executable file
385
scripts/create-single-release.ts
Executable file
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { execSync } from 'node:child_process'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
|
||||
const GITHUB_TOKEN = process.env.GH_PAT
|
||||
const REPO_OWNER = 'simstudioai'
|
||||
const REPO_NAME = 'sim'
|
||||
|
||||
if (!GITHUB_TOKEN) {
|
||||
console.error('❌ GH_PAT environment variable is required')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const targetVersion = process.argv[2]
|
||||
if (!targetVersion) {
|
||||
console.error('❌ Version argument is required')
|
||||
console.error('Usage: bun run scripts/create-single-release.ts v0.3.XX')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
})
|
||||
|
||||
interface VersionCommit {
|
||||
hash: string
|
||||
version: string
|
||||
title: string
|
||||
date: string
|
||||
author: string
|
||||
}
|
||||
|
||||
interface CommitDetail {
|
||||
hash: string
|
||||
message: string
|
||||
author: string
|
||||
githubUsername: string
|
||||
prNumber?: string
|
||||
}
|
||||
|
||||
function execCommand(command: string): string {
|
||||
try {
|
||||
return execSync(command, { encoding: 'utf8' }).trim()
|
||||
} catch (error) {
|
||||
console.error(`❌ Command failed: ${command}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function findVersionCommit(version: string): VersionCommit | null {
|
||||
console.log(`🔍 Finding commit for version ${version}...`)
|
||||
|
||||
const gitLog = execCommand('git log --oneline --format="%H|%s|%ai|%an" main')
|
||||
const lines = gitLog.split('\n').filter((line) => line.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
const [hash, message, date, author] = line.split('|')
|
||||
|
||||
const versionMatch = message.match(/^(v\d+\.\d+\.?\d*):\s*(.+)$/)
|
||||
if (versionMatch && versionMatch[1] === version) {
|
||||
return {
|
||||
hash,
|
||||
version,
|
||||
title: versionMatch[2],
|
||||
date: new Date(date).toISOString(),
|
||||
author,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findPreviousVersionCommit(currentVersion: string): VersionCommit | null {
|
||||
console.log(`🔍 Finding previous version before ${currentVersion}...`)
|
||||
|
||||
const gitLog = execCommand('git log --oneline --format="%H|%s|%ai|%an" main')
|
||||
const lines = gitLog.split('\n').filter((line) => line.trim())
|
||||
|
||||
let foundCurrent = false
|
||||
|
||||
for (const line of lines) {
|
||||
const [hash, message, date, author] = line.split('|')
|
||||
|
||||
const versionMatch = message.match(/^(v\d+\.\d+\.?\d*):\s*(.+)$/)
|
||||
if (versionMatch) {
|
||||
if (versionMatch[1] === currentVersion) {
|
||||
foundCurrent = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (foundCurrent) {
|
||||
return {
|
||||
hash,
|
||||
version: versionMatch[1],
|
||||
title: versionMatch[2],
|
||||
date: new Date(date).toISOString(),
|
||||
author,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function fetchGitHubCommitDetails(
|
||||
commitHashes: string[]
|
||||
): Promise<Map<string, CommitDetail>> {
|
||||
console.log(`🔍 Fetching GitHub commit details for ${commitHashes.length} commits...`)
|
||||
const commitMap = new Map<string, CommitDetail>()
|
||||
|
||||
for (let i = 0; i < commitHashes.length; i++) {
|
||||
const hash = commitHashes[i]
|
||||
|
||||
try {
|
||||
const { data: commit } = await octokit.rest.repos.getCommit({
|
||||
owner: REPO_OWNER,
|
||||
repo: REPO_NAME,
|
||||
ref: hash,
|
||||
})
|
||||
|
||||
const prMatch = commit.commit.message.match(/\(#(\d+)\)/)
|
||||
const prNumber = prMatch ? prMatch[1] : undefined
|
||||
|
||||
const githubUsername = commit.author?.login || commit.committer?.login || 'unknown'
|
||||
|
||||
let cleanMessage = commit.commit.message.split('\n')[0] // First line only
|
||||
if (prNumber) {
|
||||
cleanMessage = cleanMessage.replace(/\s*\(#\d+\)\s*$/, '')
|
||||
}
|
||||
|
||||
commitMap.set(hash, {
|
||||
hash,
|
||||
message: cleanMessage,
|
||||
author: commit.commit.author?.name || 'Unknown',
|
||||
githubUsername,
|
||||
prNumber,
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
} catch (error: any) {
|
||||
console.warn(`⚠️ Could not fetch commit ${hash.substring(0, 7)}: ${error?.message || error}`)
|
||||
|
||||
try {
|
||||
const gitData = execCommand(`git log --format="%s|%an" -1 ${hash}`).split('|')
|
||||
let message = gitData[0] || 'Unknown commit'
|
||||
|
||||
const prMatch = message.match(/\(#(\d+)\)/)
|
||||
const prNumber = prMatch ? prMatch[1] : undefined
|
||||
|
||||
if (prNumber) {
|
||||
message = message.replace(/\s*\(#\d+\)\s*$/, '')
|
||||
}
|
||||
|
||||
commitMap.set(hash, {
|
||||
hash,
|
||||
message,
|
||||
author: gitData[1] || 'Unknown',
|
||||
githubUsername: 'unknown',
|
||||
prNumber,
|
||||
})
|
||||
} catch (fallbackError) {
|
||||
console.error(`❌ Failed to get fallback data for ${hash.substring(0, 7)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commitMap
|
||||
}
|
||||
|
||||
async function getCommitsBetweenVersions(
|
||||
currentCommit: VersionCommit,
|
||||
previousCommit?: VersionCommit
|
||||
): Promise<CommitDetail[]> {
|
||||
try {
|
||||
let range: string
|
||||
|
||||
if (previousCommit) {
|
||||
range = `${previousCommit.hash}..${currentCommit.hash}`
|
||||
console.log(
|
||||
`🔍 Getting commits between ${previousCommit.version} and ${currentCommit.version}`
|
||||
)
|
||||
} else {
|
||||
range = `${currentCommit.hash}~10..${currentCommit.hash}`
|
||||
console.log(`🔍 Getting commits before first version ${currentCommit.version}`)
|
||||
}
|
||||
|
||||
const gitLog = execCommand(`git log --oneline --format="%H|%s" ${range}`)
|
||||
|
||||
if (!gitLog.trim()) {
|
||||
console.log(`⚠️ No commits found in range ${range}`)
|
||||
return []
|
||||
}
|
||||
|
||||
const commitEntries = gitLog.split('\n').filter((line) => line.trim())
|
||||
|
||||
const nonVersionCommits = commitEntries.filter((line) => {
|
||||
const [hash, message] = line.split('|')
|
||||
const isVersionCommit = message.match(/^v\d+\.\d+/)
|
||||
if (isVersionCommit) {
|
||||
console.log(`⏭️ Skipping version commit: ${message.substring(0, 50)}...`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
console.log(`📋 After filtering version commits: ${nonVersionCommits.length} commits`)
|
||||
|
||||
if (nonVersionCommits.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const commitHashes = nonVersionCommits.map((line) => line.split('|')[0])
|
||||
|
||||
const commitMap = await fetchGitHubCommitDetails(commitHashes)
|
||||
|
||||
return commitHashes.map((hash) => commitMap.get(hash)!).filter(Boolean)
|
||||
} catch (error) {
|
||||
console.error(`❌ Error getting commits between versions:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function categorizeCommit(message: string): 'features' | 'fixes' | 'improvements' | 'other' {
|
||||
const msgLower = message.toLowerCase()
|
||||
|
||||
if (
|
||||
msgLower.includes('feat') ||
|
||||
msgLower.includes('add') ||
|
||||
msgLower.includes('implement') ||
|
||||
msgLower.includes('new ')
|
||||
) {
|
||||
return 'features'
|
||||
}
|
||||
|
||||
if (msgLower.includes('fix') || msgLower.includes('bug') || msgLower.includes('error')) {
|
||||
return 'fixes'
|
||||
}
|
||||
|
||||
if (
|
||||
msgLower.includes('improve') ||
|
||||
msgLower.includes('enhance') ||
|
||||
msgLower.includes('update') ||
|
||||
msgLower.includes('upgrade') ||
|
||||
msgLower.includes('optimization')
|
||||
) {
|
||||
return 'improvements'
|
||||
}
|
||||
|
||||
return 'other'
|
||||
}
|
||||
|
||||
async function generateReleaseBody(
|
||||
versionCommit: VersionCommit,
|
||||
previousCommit?: VersionCommit
|
||||
): Promise<string> {
|
||||
console.log(`📝 Generating release body for ${versionCommit.version}...`)
|
||||
|
||||
const commits = await getCommitsBetweenVersions(versionCommit, previousCommit)
|
||||
|
||||
if (commits.length === 0) {
|
||||
console.log(`⚠️ No commits found, using simple format`)
|
||||
return `${versionCommit.title}
|
||||
|
||||
[View changes on GitHub](https://github.com/${REPO_OWNER}/${REPO_NAME}/compare/${previousCommit?.version || 'v1.0.0'}...${versionCommit.version})`
|
||||
}
|
||||
|
||||
console.log(`📋 Processing ${commits.length} commits for categorization`)
|
||||
|
||||
const features = commits.filter((c) => categorizeCommit(c.message) === 'features')
|
||||
const fixes = commits.filter((c) => categorizeCommit(c.message) === 'fixes')
|
||||
const improvements = commits.filter((c) => categorizeCommit(c.message) === 'improvements')
|
||||
const others = commits.filter((c) => categorizeCommit(c.message) === 'other')
|
||||
|
||||
console.log(
|
||||
`📊 Categories: ${features.length} features, ${improvements.length} improvements, ${fixes.length} fixes, ${others.length} other`
|
||||
)
|
||||
|
||||
let body = ''
|
||||
|
||||
if (features.length > 0) {
|
||||
body += '## Features\n\n'
|
||||
for (const commit of features) {
|
||||
const prLink = commit.prNumber ? ` (#${commit.prNumber})` : ''
|
||||
body += `- ${commit.message}${prLink}\n`
|
||||
}
|
||||
body += '\n'
|
||||
}
|
||||
|
||||
if (improvements.length > 0) {
|
||||
body += '## Improvements\n\n'
|
||||
for (const commit of improvements) {
|
||||
const prLink = commit.prNumber ? ` (#${commit.prNumber})` : ''
|
||||
body += `- ${commit.message}${prLink}\n`
|
||||
}
|
||||
body += '\n'
|
||||
}
|
||||
|
||||
if (fixes.length > 0) {
|
||||
body += '## Bug Fixes\n\n'
|
||||
for (const commit of fixes) {
|
||||
const prLink = commit.prNumber ? ` (#${commit.prNumber})` : ''
|
||||
body += `- ${commit.message}${prLink}\n`
|
||||
}
|
||||
body += '\n'
|
||||
}
|
||||
|
||||
if (others.length > 0) {
|
||||
body += '## Other Changes\n\n'
|
||||
for (const commit of others) {
|
||||
const prLink = commit.prNumber ? ` (#${commit.prNumber})` : ''
|
||||
body += `- ${commit.message}${prLink}\n`
|
||||
}
|
||||
body += '\n'
|
||||
}
|
||||
|
||||
const uniqueContributors = new Set<string>()
|
||||
commits.forEach((commit) => {
|
||||
if (commit.githubUsername && commit.githubUsername !== 'unknown') {
|
||||
uniqueContributors.add(commit.githubUsername)
|
||||
}
|
||||
})
|
||||
|
||||
if (uniqueContributors.size > 0) {
|
||||
body += '## Contributors\n\n'
|
||||
for (const contributor of Array.from(uniqueContributors).sort()) {
|
||||
body += `- @${contributor}\n`
|
||||
}
|
||||
body += '\n'
|
||||
}
|
||||
|
||||
body += `[View changes on GitHub](https://github.com/${REPO_OWNER}/${REPO_NAME}/compare/${previousCommit?.version || 'v1.0.0'}...${versionCommit.version})`
|
||||
|
||||
return body.trim()
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log(`🚀 Creating single release for ${targetVersion}...`)
|
||||
|
||||
const versionCommit = findVersionCommit(targetVersion)
|
||||
if (!versionCommit) {
|
||||
console.error(`❌ No commit found for version ${targetVersion}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ Found version commit: ${versionCommit.hash.substring(0, 7)} - ${versionCommit.title}`
|
||||
)
|
||||
|
||||
const previousCommit = findPreviousVersionCommit(targetVersion)
|
||||
if (previousCommit) {
|
||||
console.log(`✅ Found previous version: ${previousCommit.version}`)
|
||||
} else {
|
||||
console.log(`ℹ️ No previous version found (this might be the first release)`)
|
||||
}
|
||||
|
||||
const releaseBody = await generateReleaseBody(versionCommit, previousCommit || undefined)
|
||||
|
||||
console.log(`🚀 Creating GitHub release for ${targetVersion}...`)
|
||||
|
||||
await octokit.rest.repos.createRelease({
|
||||
owner: REPO_OWNER,
|
||||
repo: REPO_NAME,
|
||||
tag_name: targetVersion,
|
||||
name: targetVersion,
|
||||
body: releaseBody,
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
target_commitish: versionCommit.hash,
|
||||
})
|
||||
|
||||
console.log(`✅ Successfully created release: ${targetVersion}`)
|
||||
console.log(
|
||||
`🔗 View release: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/tag/${targetVersion}`
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('❌ Script failed:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user