diff --git a/apps/sim/app/(landing)/actions/github.ts b/apps/sim/app/(landing)/actions/github.ts
index 38aea8538b..efd28a2124 100644
--- a/apps/sim/app/(landing)/actions/github.ts
+++ b/apps/sim/app/(landing)/actions/github.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
-const DEFAULT_STARS = '15k'
+const DEFAULT_STARS = '15.4k'
const logger = createLogger('GitHubStars')
diff --git a/apps/sim/app/(landing)/components/footer/footer.tsx b/apps/sim/app/(landing)/components/footer/footer.tsx
index ca82c6f7ab..5d194c40ac 100644
--- a/apps/sim/app/(landing)/components/footer/footer.tsx
+++ b/apps/sim/app/(landing)/components/footer/footer.tsx
@@ -214,6 +214,12 @@ export default function Footer({ fullWidth = false }: FooterProps) {
>
Enterprise
+
+ Changelog
+
/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) => `
+ -
+ ${escapeXml(r.name || r.tag_name)}
+ ${r.html_url}
+ ${r.html_url}
+ ${new Date(r.published_at).toUTCString()}
+
+
+ `
+ )
+ .join('')
+
+ const xml = `
+
+
+ Sim Changelog
+ https://sim.dev/changelog
+ Latest changes, fixes and updates in Sim.
+ en-us
+ ${items}
+
+ `
+
+ 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 })
+ }
+}
diff --git a/apps/sim/app/changelog/components/changelog-content.tsx b/apps/sim/app/changelog/components/changelog-content.tsx
new file mode 100644
index 0000000000..6fea9d5581
--- /dev/null
+++ b/apps/sim/app/changelog/components/changelog-content.tsx
@@ -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 (
+
+
+ {/* Left intro panel */}
+
+
+
+
+
+
+ Changelog
+
+
+ Stay up-to-date with the latest features, improvements, and bug fixes in Sim. All
+ changes are documented here with detailed release notes.
+
+
+
+
+
+
+ View on GitHub
+
+
+
+ Documentation
+
+
+
+ RSS Feed
+
+
+
+
+
+ {/* Right timeline */}
+
+
+
+ )
+}
diff --git a/apps/sim/app/changelog/components/timeline-list.tsx b/apps/sim/app/changelog/components/timeline-list.tsx
new file mode 100644
index 0000000000..ecb306c56f
--- /dev/null
+++ b/apps/sim/app/changelog/components/timeline-list.tsx
@@ -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(initialEntries)
+ const [page, setPage] = React.useState(1)
+ const [loading, setLoading] = React.useState(false)
+ const [done, setDone] = React.useState(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 (
+
+ {entries.map((entry) => (
+
+
+
+ {entry.tag}
+
+
+ {new Date(entry.date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })}
+
+
+
+
+
+ isContributorsLabel(children) ? null : (
+
+ {children}
+
+ ),
+ h3: ({ children, ...props }) =>
+ isContributorsLabel(children) ? null : (
+
+ {children}
+
+ ),
+ ul: ({ children, ...props }) => (
+
+ ),
+ li: ({ children, ...props }) => {
+ const text = String(children)
+ if (/^\s*contributors\s*:?\s*$/i.test(text)) return null
+ return (
+
+ {children}
+
+ )
+ },
+ p: ({ children, ...props }) =>
+ /^\s*contributors\s*:?\s*$/i.test(String(children)) ? null : (
+
+ {children}
+
+ ),
+ strong: ({ children, ...props }) => (
+
+ {children}
+
+ ),
+ code: ({ children, ...props }) => (
+
+ {children}
+
+ ),
+ img: () => null,
+ a: ({ className, ...props }: any) => (
+
+ ),
+ }}
+ >
+ {cleanMarkdown(entry.content)}
+
+
+
+ {entry.contributors && entry.contributors.length > 0 && (
+
+ {entry.contributors.slice(0, 5).map((contributor) => (
+
+
+ {contributor.slice(0, 2).toUpperCase()}
+
+ ))}
+ {entry.contributors.length > 5 && (
+
+ +{entry.contributors.length - 5}
+
+ )}
+
+ )}
+
+ ))}
+
+ {!done && (
+
+
+
+ )}
+
+ )
+}
diff --git a/apps/sim/app/changelog/layout.tsx b/apps/sim/app/changelog/layout.tsx
new file mode 100644
index 0000000000..57508fe7a7
--- /dev/null
+++ b/apps/sim/app/changelog/layout.tsx
@@ -0,0 +1,10 @@
+import Nav from '@/app/(landing)/components/nav/nav'
+
+export default function ChangelogLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ )
+}
diff --git a/apps/sim/app/changelog/page.tsx b/apps/sim/app/changelog/page.tsx
new file mode 100644
index 0000000000..39dbfeaa79
--- /dev/null
+++ b/apps/sim/app/changelog/page.tsx
@@ -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
+}
diff --git a/apps/sim/app/conditional-theme-provider.tsx b/apps/sim/app/conditional-theme-provider.tsx
index 332b094ec1..fedddf40f2 100644
--- a/apps/sim/app/conditional-theme-provider.tsx
+++ b/apps/sim/app/conditional-theme-provider.tsx
@@ -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
diff --git a/apps/sim/components/ui/avatar.tsx b/apps/sim/components/ui/avatar.tsx
index 38b7a5e985..6ecb201311 100644
--- a/apps/sim/components/ui/avatar.tsx
+++ b/apps/sim/components/ui/avatar.tsx
@@ -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,
React.ComponentPropsWithoutRef
@@ -22,7 +40,7 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -35,7 +53,7 @@ const AvatarFallback = React.forwardRef<
) {
+ return (
+
+ )
+}
+
+function AvatarStatus({
+ className,
+ variant,
+ ...props
+}: React.HTMLAttributes & VariantProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback, AvatarIndicator, AvatarStatus, avatarStatusVariants }
diff --git a/apps/sim/lib/security/csp.ts b/apps/sim/lib/security/csp.ts
index 42d14e9ce1..a4d117b03a 100644
--- a/apps/sim/lib/security/csp.ts
+++ b/apps/sim/lib/security/csp.ts
@@ -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';
diff --git a/bun.lock b/bun.lock
index f6d32d8e77..da40564970 100644
--- a/bun.lock
+++ b/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=="],
diff --git a/package.json b/package.json
index 3e4b55fdd8..80ba33ce51 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/scripts/create-single-release.ts b/scripts/create-single-release.ts
new file mode 100755
index 0000000000..3e6d90c56f
--- /dev/null
+++ b/scripts/create-single-release.ts
@@ -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