mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
fix(web): link release notes to GitHub (#2114)
* fix(web): link release notes to GitHub * test(web): verify release link safety contract
This commit is contained in:
@@ -4,5 +4,8 @@ import sitemap from '@astrojs/sitemap'
|
||||
export default defineConfig({
|
||||
site: 'https://openclaude.gitlawb.com',
|
||||
trailingSlash: 'always',
|
||||
redirects: {
|
||||
'/changelog/': 'https://github.com/Gitlawb/openclaude/releases',
|
||||
},
|
||||
integrations: [sitemap()],
|
||||
})
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
||||
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { verifyDist, npmFreshnessFailure } from './verify-dist'
|
||||
import { verifyDist } from './verify-dist'
|
||||
import { SITE } from '../src/data/site'
|
||||
import { docsPages } from '../src/data/docsNav'
|
||||
import { releases, releaseUrl } from '../src/data/releases'
|
||||
import { heroes } from '../src/data/buddy'
|
||||
import { partners, community } from '../src/data/partners'
|
||||
|
||||
@@ -18,23 +17,17 @@ function writePage(dist: string, route: string, html: string): void {
|
||||
|
||||
/** Build a minimal dist/ that satisfies every verifyDist assertion. */
|
||||
function writeValidFixture(dist: string): void {
|
||||
const navLinks = ['/buddy/', '/changelog/'].map(h => `<a href="${h}">x</a>`).join('')
|
||||
const navLinks = [`<a href="/buddy/">x</a>`, `<a href="${SITE.releasesUrl}" target="_blank" rel="noopener">x</a>`].join('')
|
||||
writePage(
|
||||
dist,
|
||||
'/',
|
||||
`<html>v${SITE.version}${navLinks}${partners
|
||||
`<html>${navLinks}${partners
|
||||
.map(p => `<a href="${p.url}"><img src="${p.logo}"></a>`)
|
||||
.join('')}${community.map(c => `<a href="${c.url}">x</a>`).join('')}</html>`,
|
||||
)
|
||||
const sidebar = docsPages.map(p => `<a href="${p.href}">x</a>`).join('')
|
||||
const sidebar = `${docsPages.map(p => `<a href="${p.href}">x</a>`).join('')}<a href="${SITE.releasesUrl}" target="_blank" rel="noopener">x</a>`
|
||||
for (const p of docsPages) writePage(dist, p.href, `<html>${sidebar}</html>`)
|
||||
writePage(
|
||||
dist,
|
||||
'/changelog/',
|
||||
`<html>v${SITE.version}${releases
|
||||
.map(r => `<a href="${releaseUrl(r.version)}">v${r.version}</a>`)
|
||||
.join('')}</html>`,
|
||||
)
|
||||
writePage(dist, '/changelog/', `<html><meta http-equiv="refresh" content="0;url=${SITE.releasesUrl}"><meta name="robots" content="noindex">${SITE.releasesUrl}</html>`)
|
||||
writePage(
|
||||
dist,
|
||||
'/buddy/',
|
||||
@@ -44,7 +37,7 @@ function writeValidFixture(dist: string): void {
|
||||
for (const h of heroes) writeFileSync(join(dist, 'buddy', `${h.id}.svg`), '<svg/>')
|
||||
writeFileSync(
|
||||
join(dist, 'sitemap-0.xml'),
|
||||
`<urlset>${['/buddy/', '/changelog/'].map(r => `<loc>${SITE.url}${r}</loc>`).join('')}</urlset>`,
|
||||
`<urlset><loc>${SITE.url}/buddy/</loc></urlset>`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,35 +52,6 @@ function withFixture(mutate: (dist: string) => void): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
describe('release data', () => {
|
||||
test('keeps releases newest first', () => {
|
||||
const versions = releases.map(release => release.version.split('.').map(Number))
|
||||
const sorted = [...versions].sort((a, b) => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (a[i] !== b[i]) return (b[i] ?? 0) - (a[i] ?? 0)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
expect(versions).toEqual(sorted)
|
||||
})
|
||||
|
||||
test('lists the 0.27.0 release with its curated highlights', () => {
|
||||
expect(releases.find(release => release.version === '0.27.0')).toEqual({
|
||||
version: '0.27.0',
|
||||
date: '2026-07-30',
|
||||
theme: 'auth-ready local proxies and a refreshed web identity',
|
||||
highlights: [
|
||||
'opt-in loopback proxy hosts preserve subscription OAuth authentication',
|
||||
'new Ling 3.0 Flash and Macaron V1 Tall catalog entries',
|
||||
'centered startup logo and updated Ember Block O web branding',
|
||||
'agents can spawn subagents from multi-repository parent sessions',
|
||||
'more reliable tool-failure guard, SDK permission-timeout reporting, stats, and status UI',
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('verifyDist', () => {
|
||||
test('passes on a complete fixture', () => {
|
||||
expect(withFixture(() => {})).toEqual([])
|
||||
@@ -107,13 +71,50 @@ describe('verifyDist', () => {
|
||||
|
||||
test('flags a docs sidebar that lost a navigation link', () => {
|
||||
const failures = withFixture(dist => {
|
||||
const sidebar = docsPages
|
||||
.filter(p => p.href !== '/changelog/')
|
||||
.map(p => `<a href="${p.href}">x</a>`)
|
||||
.join('')
|
||||
const sidebar = docsPages.map(p => `<a href="${p.href}">x</a>`).join('')
|
||||
writePage(dist, '/docs/', `<html>${sidebar}</html>`)
|
||||
})
|
||||
expect(failures).toContain('docs sidebar link /changelog/: missing "href=\\"/changelog/\\""')
|
||||
expect(failures).toContain('docs release notes link: missing safe new-tab release link')
|
||||
})
|
||||
|
||||
test('flags a landing release link that lost target=_blank', () => {
|
||||
const failures = withFixture(dist => {
|
||||
const index = join(dist, 'index.html')
|
||||
writeFileSync(index, readFileSync(index, 'utf8').replace(' target="_blank" rel="noopener"', ' rel="noopener"'))
|
||||
})
|
||||
expect(failures).toContain('landing release notes link: missing safe new-tab release link')
|
||||
})
|
||||
|
||||
test('flags a docs release link that lost rel=noopener', () => {
|
||||
const failures = withFixture(dist => {
|
||||
const docs = join(dist, 'docs', 'index.html')
|
||||
writeFileSync(docs, readFileSync(docs, 'utf8').replace(' target="_blank" rel="noopener"', ' target="_blank"'))
|
||||
})
|
||||
expect(failures).toContain('docs release notes link: missing safe new-tab release link')
|
||||
})
|
||||
|
||||
test('flags an unsafe release link even when another release link is safe', () => {
|
||||
const failures = withFixture(dist => {
|
||||
const index = join(dist, 'index.html')
|
||||
writeFileSync(index, `${readFileSync(index, 'utf8')}<a href="${SITE.releasesUrl}" target="_blank">x</a>`)
|
||||
})
|
||||
expect(failures).toContain('landing release notes link: missing safe new-tab release link')
|
||||
})
|
||||
|
||||
test('flags a legacy changelog page that lost its redirect', () => {
|
||||
const failures = withFixture(dist => {
|
||||
const changelog = join(dist, 'changelog', 'index.html')
|
||||
writeFileSync(changelog, readFileSync(changelog, 'utf8').replace(`<meta http-equiv="refresh" content="0;url=${SITE.releasesUrl}">`, ''))
|
||||
})
|
||||
expect(failures).toContain(`legacy changelog redirect: missing "<meta http-equiv=\\"refresh\\" content=\\"0;url=${SITE.releasesUrl}\\">"`)
|
||||
})
|
||||
|
||||
test('flags a legacy changelog redirect that lost noindex', () => {
|
||||
const failures = withFixture(dist => {
|
||||
const changelog = join(dist, 'changelog', 'index.html')
|
||||
writeFileSync(changelog, readFileSync(changelog, 'utf8').replace('<meta name="robots" content="noindex">', ''))
|
||||
})
|
||||
expect(failures).toContain('legacy changelog noindex: missing "<meta name=\\"robots\\" content=\\"noindex\\">"')
|
||||
})
|
||||
|
||||
test('flags a missing sprite asset', () => {
|
||||
@@ -124,7 +125,7 @@ describe('verifyDist', () => {
|
||||
|
||||
test('flags a stale landing page missing a partner link', () => {
|
||||
const failures = withFixture(dist => {
|
||||
const html = `<html>v${SITE.version}<a href="/buddy/">x</a><a href="/changelog/">x</a>${community
|
||||
const html = `<html><a href="/buddy/">x</a><a href="${SITE.releasesUrl}" target="_blank">x</a>${community
|
||||
.map(c => `<a href="${c.url}">x</a>`)
|
||||
.join('')}</html>`
|
||||
writeFileSync(join(dist, 'index.html'), html)
|
||||
@@ -137,7 +138,6 @@ describe('verifyDist', () => {
|
||||
writeFileSync(join(dist, 'sitemap-0.xml'), `<urlset><loc>${SITE.url}/</loc></urlset>`),
|
||||
)
|
||||
expect(failures.some(f => f.startsWith('sitemap entry /buddy/'))).toBe(true)
|
||||
expect(failures.some(f => f.startsWith('sitemap entry /changelog/'))).toBe(true)
|
||||
})
|
||||
|
||||
test('flags a missing sitemap', () => {
|
||||
@@ -145,46 +145,4 @@ describe('verifyDist', () => {
|
||||
expect(failures).toContain('missing dist/sitemap-0.xml')
|
||||
})
|
||||
|
||||
test('flags a changelog entry that lost its release URL', () => {
|
||||
const failures = withFixture(dist => {
|
||||
const html = `<html>v${SITE.version}${releases.map(r => `v${r.version}`).join(' ')}</html>`
|
||||
writeFileSync(join(dist, 'changelog', 'index.html'), html)
|
||||
})
|
||||
expect(failures.some(f => f.startsWith('changelog release URL'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('npmFreshnessFailure', () => {
|
||||
function fetchReturning(body: unknown, ok = true): typeof fetch {
|
||||
return (() =>
|
||||
Promise.resolve({ ok, json: () => Promise.resolve(body) } as Response)) as typeof fetch
|
||||
}
|
||||
|
||||
test('passes when npm matches the site version', async () => {
|
||||
expect(await npmFreshnessFailure(fetchReturning({ version: SITE.version }))).toBeNull()
|
||||
})
|
||||
|
||||
test('passes when the site is ahead of npm (release PR before publish)', async () => {
|
||||
expect(await npmFreshnessFailure(fetchReturning({ version: '0.1.0' }))).toBeNull()
|
||||
})
|
||||
|
||||
test('fails when npm has a newer release than releases.ts', async () => {
|
||||
const failure = await npmFreshnessFailure(fetchReturning({ version: '999.0.0' }))
|
||||
expect(failure).toContain('999.0.0')
|
||||
expect(failure).toContain('web/src/data/releases.ts')
|
||||
expect(failure).toContain('do not patch it from unrelated PRs')
|
||||
})
|
||||
|
||||
test('skips on network failure instead of breaking the build', async () => {
|
||||
const offline = (() => Promise.reject(new Error('offline'))) as typeof fetch
|
||||
expect(await npmFreshnessFailure(offline)).toBeNull()
|
||||
})
|
||||
|
||||
test('skips on a malformed registry response', async () => {
|
||||
expect(await npmFreshnessFailure(fetchReturning({}))).toBeNull()
|
||||
expect(await npmFreshnessFailure(fetchReturning({ version: 'not-semver' }))).toBeNull()
|
||||
expect(await npmFreshnessFailure(fetchReturning({ version: '01.2.3' }))).toBeNull()
|
||||
expect(await npmFreshnessFailure(fetchReturning({ version: '999.00.0' }))).toBeNull()
|
||||
expect(await npmFreshnessFailure(fetchReturning({}, false))).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
+18
-50
@@ -1,13 +1,12 @@
|
||||
// Post-build guard, run by `bun run build` after `astro build`.
|
||||
// Asserts that the typed data files actually drive the rendered output:
|
||||
// version propagation, navigation, and the /, /buddy/, /changelog/ routes.
|
||||
// navigation and the / and /buddy/ routes.
|
||||
// Kept as a pure function so verify-dist.test.ts can exercise it on fixtures.
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { SITE } from '../src/data/site'
|
||||
import { docsPages } from '../src/data/docsNav'
|
||||
import { releases, releaseUrl } from '../src/data/releases'
|
||||
import { heroes } from '../src/data/buddy'
|
||||
import { partners, community } from '../src/data/partners'
|
||||
|
||||
@@ -34,9 +33,13 @@ export function verifyDist(dist: string): string[] {
|
||||
if (html !== '' && !html.includes(needle)) failures.push(`${why}: missing ${JSON.stringify(needle)}`)
|
||||
}
|
||||
|
||||
// ── version propagation (site.ts derives from the newest releases entry) ─
|
||||
function expectReleaseLinks(html: string, why: string): void {
|
||||
const releaseLinks = (html.match(/<a\b[^>]*>/g) ?? []).filter(link => link.includes(`href="${SITE.releasesUrl}"`))
|
||||
if (html !== '' && (releaseLinks.length === 0 || releaseLinks.some(link => !link.includes('target="_blank"') || !link.includes('rel="noopener"'))))
|
||||
failures.push(`${why}: missing safe new-tab release link`)
|
||||
}
|
||||
|
||||
const index = page('/')
|
||||
expect(index, `v${SITE.version}`, 'landing version')
|
||||
|
||||
// ── navigation exposes every docsNav route, in data AND rendered output ──
|
||||
const docsIndex = page('/docs/')
|
||||
@@ -44,18 +47,16 @@ export function verifyDist(dist: string): string[] {
|
||||
page(p.href) // records a failure if the route didn't build
|
||||
expect(docsIndex, `href="${p.href}"`, `docs sidebar link ${p.href}`)
|
||||
}
|
||||
for (const href of ['/buddy/', '/changelog/'] as const) {
|
||||
if (!docsPages.some(p => p.href === href)) failures.push(`docsNav missing ${href}`)
|
||||
expect(index, `href="${href}"`, `landing nav link ${href}`)
|
||||
}
|
||||
if (!docsPages.some(p => p.href === '/buddy/')) failures.push('docsNav missing /buddy/')
|
||||
expect(index, 'href="/buddy/"', 'landing nav link /buddy/')
|
||||
expectReleaseLinks(index, 'landing release notes link')
|
||||
expectReleaseLinks(docsIndex, 'docs release notes link')
|
||||
|
||||
// ── /changelog/: every release renders with its GitHub release URL ───────
|
||||
const changelog = page('/changelog/')
|
||||
for (const r of releases) {
|
||||
expect(changelog, `v${r.version}`, `changelog release ${r.version}`)
|
||||
expect(changelog, releaseUrl(r.version), `changelog release URL ${r.version}`)
|
||||
}
|
||||
expect(changelog, `v${SITE.version}`, 'changelog current-version pill')
|
||||
// ── legacy route continues on the canonical GitHub Releases page ─────────
|
||||
const legacyChangelog = page('/changelog/')
|
||||
expect(legacyChangelog, SITE.releasesUrl, 'legacy changelog redirect')
|
||||
expect(legacyChangelog, `<meta http-equiv="refresh" content="0;url=${SITE.releasesUrl}">`, 'legacy changelog redirect')
|
||||
expect(legacyChangelog, '<meta name="robots" content="noindex">', 'legacy changelog noindex')
|
||||
|
||||
// ── /buddy/: every hero renders with its sprite ──────────────────────────
|
||||
const buddy = page('/buddy/')
|
||||
@@ -77,7 +78,7 @@ export function verifyDist(dist: string): string[] {
|
||||
const sitemapFile = join(dist, 'sitemap-0.xml')
|
||||
if (existsSync(sitemapFile)) {
|
||||
const sitemap = readFileSync(sitemapFile, 'utf8')
|
||||
for (const route of ['/buddy/', '/changelog/'])
|
||||
for (const route of ['/buddy/'])
|
||||
expect(sitemap, `${SITE.url}${route}`, `sitemap entry ${route}`)
|
||||
} else {
|
||||
failures.push('missing dist/sitemap-0.xml')
|
||||
@@ -86,45 +87,12 @@ export function verifyDist(dist: string): string[] {
|
||||
return [...new Set(failures)]
|
||||
}
|
||||
|
||||
function newerThan(a: string, b: string): boolean {
|
||||
const pa = a.split('.').map(Number)
|
||||
const pb = b.split('.').map(Number)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// With web/ standalone, releases.ts is the only version source — this is the
|
||||
// guard against it silently going stale. Best-effort by design: an unreachable
|
||||
// registry (offline CI, npm outage) skips the check rather than failing the
|
||||
// build; only a *confirmed newer* npm release fails. A site version ahead of
|
||||
// npm is allowed so a release PR can land before the publish completes.
|
||||
export async function npmFreshnessFailure(fetchImpl: typeof fetch = fetch): Promise<string | null> {
|
||||
let published: string
|
||||
try {
|
||||
const res = await fetchImpl('https://registry.npmjs.org/@gitlawb/openclaude/latest', {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (!res.ok) return null
|
||||
published = ((await res.json()) as { version?: string }).version ?? ''
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(published)) return null
|
||||
if (newerThan(published, SITE.version))
|
||||
return `npm latest ${published} is newer than site version ${SITE.version} — releases.ts is stale; do not patch it from unrelated PRs (release/web process owns web/src/data/releases.ts)`
|
||||
return null
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const failures = verifyDist(join(import.meta.dir, '..', 'dist'))
|
||||
const stale = await npmFreshnessFailure()
|
||||
if (stale) failures.push(stale)
|
||||
if (failures.length > 0) {
|
||||
console.error(`verify-dist: ${failures.length} failure(s)`)
|
||||
for (const f of failures) console.error(` ✗ ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('verify-dist: ok — version, nav, buddy, changelog, partners all verified')
|
||||
console.log('verify-dist: ok — navigation, buddy, and partners verified')
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const path = Astro.url.pathname
|
||||
<ul>
|
||||
{group.items.map(item => (
|
||||
<li>
|
||||
<a href={item.href} aria-current={path === item.href ? 'page' : undefined}>
|
||||
<a href={item.href} aria-current={path === item.href ? 'page' : undefined} rel={item.newTab ? 'noopener' : undefined} target={item.newTab ? '_blank' : undefined}>
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -24,7 +24,7 @@ const columns = [
|
||||
{
|
||||
title: 'project',
|
||||
links: [
|
||||
{ href: '/changelog/', label: "what's new" },
|
||||
{ href: SITE.releasesUrl, label: "what's new", newTab: true },
|
||||
{ href: '/buddy/', label: 'buddy' },
|
||||
{ href: SITE.github, label: 'github' },
|
||||
{ href: SITE.npmUrl, label: 'npm' },
|
||||
@@ -54,7 +54,7 @@ const columns = [
|
||||
<ul>
|
||||
{col.links.map(l => (
|
||||
<li>
|
||||
<a href={l.href} rel={l.href.startsWith('http') ? 'noopener' : undefined}>{l.label}</a>
|
||||
<a href={l.href} rel={l.href.startsWith('http') ? 'noopener' : undefined} target={l.newTab ? '_blank' : undefined}>{l.label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -65,7 +65,6 @@ const columns = [
|
||||
<span class="brand">
|
||||
<img src="/openclaude.png" alt="" width="18" height="18" />
|
||||
<span>openclaude</span>
|
||||
<span class="ver">v{SITE.version}</span>
|
||||
</span>
|
||||
<span class="sep">·</span>
|
||||
<span>open source coding agent, aligned with <a href={SITE.gitlawb} rel="noopener">gitlawb</a></span>
|
||||
|
||||
@@ -5,7 +5,7 @@ const links = [
|
||||
{ href: '/docs/', label: 'docs' },
|
||||
{ href: '/docs/providers/', label: 'providers' },
|
||||
{ href: '/buddy/', label: 'buddy' },
|
||||
{ href: '/changelog/', label: "what's new" },
|
||||
{ href: SITE.releasesUrl, label: "what's new", newTab: true },
|
||||
{ href: SITE.github, label: 'github' },
|
||||
{ href: SITE.gitlawb, label: 'gitlawb' },
|
||||
]
|
||||
@@ -18,7 +18,6 @@ const path = Astro.url.pathname
|
||||
<a class="brand" href="/" aria-label="openclaude home">
|
||||
<img src="/openclaude.png" alt="" width="22" height="22" />
|
||||
<span>openclaude</span>
|
||||
<span class="ver">v{SITE.version}</span>
|
||||
</a>
|
||||
<nav class="nav-links" aria-label="primary">
|
||||
{links.map(l => (
|
||||
@@ -26,6 +25,7 @@ const path = Astro.url.pathname
|
||||
href={l.href}
|
||||
aria-current={path === l.href ? 'page' : undefined}
|
||||
rel={l.href.startsWith('http') ? 'noopener' : undefined}
|
||||
target={l.newTab ? '_blank' : undefined}
|
||||
>{l.label}</a>
|
||||
))}
|
||||
<button type="button" class="theme-toggle" data-theme-toggle aria-live="polite">[light]</button>
|
||||
@@ -36,7 +36,7 @@ const path = Astro.url.pathname
|
||||
</div>
|
||||
<div class="mobile-menu" id="mobile-menu">
|
||||
{links.map(l => (
|
||||
<a href={l.href} rel={l.href.startsWith('http') ? 'noopener' : undefined}>{l.label}</a>
|
||||
<a href={l.href} rel={l.href.startsWith('http') ? 'noopener' : undefined} target={l.newTab ? '_blank' : undefined}>{l.label}</a>
|
||||
))}
|
||||
<a href="#" data-theme-toggle role="button">[light]</a>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { SITE } from './site'
|
||||
|
||||
export interface DocsNavItem {
|
||||
title: string
|
||||
href: string
|
||||
newTab?: boolean
|
||||
}
|
||||
|
||||
export interface DocsNavGroup {
|
||||
@@ -31,13 +34,13 @@ export const docsNav: DocsNavGroup[] = [
|
||||
{
|
||||
group: 'more',
|
||||
items: [
|
||||
{ title: "What's new", href: '/changelog/' },
|
||||
{ title: "What's new", href: SITE.releasesUrl, newTab: true },
|
||||
{ title: 'Buddy', href: '/buddy/' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const docsPages: DocsNavItem[] = docsNav.flatMap(g => g.items)
|
||||
export const docsPages: DocsNavItem[] = docsNav.flatMap(g => g.items).filter(item => !item.newTab)
|
||||
|
||||
export function pagerFor(href: string): { prev?: DocsNavItem; next?: DocsNavItem } {
|
||||
const i = docsPages.findIndex(p => p.href === href)
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
// Curated release highlights for the /changelog page. Full notes live on
|
||||
// GitHub releases; this list is the hand-picked "what actually matters"
|
||||
// per minor version. Newest first.
|
||||
//
|
||||
// Ownership: release/web process only (release automation and dedicated web
|
||||
// release PRs). Do not edit this file from ordinary feature or bugfix PRs,
|
||||
// including to silence web CI when npm is ahead of the site version.
|
||||
|
||||
export interface Release {
|
||||
version: string
|
||||
date: string
|
||||
/** one-line theme of the release */
|
||||
theme: string
|
||||
highlights: string[]
|
||||
}
|
||||
|
||||
export const RELEASES_URL = 'https://github.com/Gitlawb/openclaude/releases'
|
||||
|
||||
export function releaseUrl(version: string): string {
|
||||
return `${RELEASES_URL}/tag/v${version}`
|
||||
}
|
||||
|
||||
export const releases: Release[] = [
|
||||
{
|
||||
version: '0.27.0',
|
||||
date: '2026-07-30',
|
||||
theme: 'auth-ready local proxies and a refreshed web identity',
|
||||
highlights: [
|
||||
'opt-in loopback proxy hosts preserve subscription OAuth authentication',
|
||||
'new Ling 3.0 Flash and Macaron V1 Tall catalog entries',
|
||||
'centered startup logo and updated Ember Block O web branding',
|
||||
'agents can spawn subagents from multi-repository parent sessions',
|
||||
'more reliable tool-failure guard, SDK permission-timeout reporting, stats, and status UI',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.26.0',
|
||||
date: '2026-07-27',
|
||||
theme: 'polish — steadier long turns, sharper streaming feedback',
|
||||
highlights: [
|
||||
'long-running tools stay active instead of tripping the query guard',
|
||||
'streaming token counts appear immediately, not after the first chunk',
|
||||
'AI/ML API client hardening: passwordless methods and response-shape guards',
|
||||
'windows: tolerate EPERM from mkdir on drive roots',
|
||||
'slash-command arguments insert literally — no more accidental regex references',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.25.0',
|
||||
date: '2026-07-20',
|
||||
theme: 'buddy companions, GPT-5.6, and a leaner install',
|
||||
highlights: [
|
||||
'buddy: pixel-art hero companions with signature Enter animations — /buddy to hatch one',
|
||||
'GPT-5.6 family models on Codex, routed through the OpenAI Responses API',
|
||||
'first-run onboarding experience for third-party providers',
|
||||
'token optimization: universal tool compression, doom-loop detection, configurable compaction',
|
||||
'zero-warning npm install, enforced by a release gate and daily 3-OS checks',
|
||||
'context bar shows live token counts — ctx 74K/200K (37%)',
|
||||
'new providers: LongCat, AI/ML API foundation, Kimi K3 context variants',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.24.0',
|
||||
date: '2026-07-14',
|
||||
theme: 'effort control and per-model tuning',
|
||||
highlights: [
|
||||
'ultrathink keyword detection and the ultracode effort level',
|
||||
'Cloudflare Workers AI provider integration',
|
||||
'per-model context_window and max_output_tokens overrides in settings',
|
||||
'Codex OAuth: manual callback URL paste for SSH and remote sessions',
|
||||
'/doctor gains WebSearch backend diagnostics',
|
||||
'/model surfaces inactive provider profiles',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.23.0',
|
||||
date: '2026-07-07',
|
||||
theme: 'codebase intelligence and skills everywhere',
|
||||
highlights: [
|
||||
'repo map codebase intelligence',
|
||||
'smart auto-routing: per-turn simple-vs-strong model selection',
|
||||
'AI/ML API joins as a first-class provider',
|
||||
'local skill CLI support plus a native-TypeScript PDF generation skill',
|
||||
'ship a zero-warning, minimal install',
|
||||
'openclaude config fully isolated from Claude Code',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.22.0',
|
||||
date: '2026-07-06',
|
||||
theme: 'honest feedback and LSP visibility',
|
||||
highlights: [
|
||||
'LSP diagnostics captured and exposed to the agent',
|
||||
'task reports render as markdown',
|
||||
'honest-feedback UX pass: visible retries, statusline truncation marker, hint grace period',
|
||||
'resume picker groups branched sessions',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.21.0',
|
||||
date: '2026-06-30',
|
||||
theme: 'session branching and effort routing',
|
||||
highlights: [
|
||||
'/branch forks a conversation into a new session',
|
||||
'/set-context-window and /clear-context-window commands',
|
||||
'model-level reasoning effort routing',
|
||||
'per-agent step limits and deterministic session task reports',
|
||||
'Claude Opus 4.8 support; ClinePass gateway provider',
|
||||
'recover GLM/Qwen tool calls emitted as XML text',
|
||||
'perf: minified CLI bundle and lazy provider catalog loading',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.20.0',
|
||||
date: '2026-06-24',
|
||||
theme: 'background sessions and the bughunter goes public',
|
||||
highlights: [
|
||||
'local background sessions — run agents while you keep typing',
|
||||
'/bughunter public, with /bughunter-security and /bughunter-perf variants',
|
||||
'per-agent model assignment from the /agents menu',
|
||||
'/update command with package-manager auto-detection',
|
||||
'auto-detect and persist project conventions to the wiki',
|
||||
'OpenAI-compatible credential pool failover; OPENCLAUDE_CONFIG_DIR override',
|
||||
'GLM 5.2 across Z.AI, Fireworks, Atlas Cloud, and Opengateway',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.19.0',
|
||||
date: '2026-06-16',
|
||||
theme: 'context visibility and new backends',
|
||||
highlights: [
|
||||
'/ctx context-window visualization and token bars in /cost',
|
||||
'NEAR AI provider integration',
|
||||
'native Gemini Vertex client and auth helpers',
|
||||
'compactModel option — use a separate model for compaction',
|
||||
'auto-compact prompt on /resume with a determinate progress bar',
|
||||
'redacted diagnostic issue reports',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// web/ builds standalone (no access to the repo root), so the newest changelog
|
||||
// entry doubles as the site's displayed version. verify-dist cross-checks it
|
||||
// against the published npm version so this list can't silently go stale.
|
||||
export const latestVersion = releases[0].version
|
||||
@@ -1,5 +1,3 @@
|
||||
import { latestVersion } from './releases'
|
||||
|
||||
export const SITE = {
|
||||
url: 'https://openclaude.gitlawb.com',
|
||||
name: 'openclaude',
|
||||
@@ -9,9 +7,9 @@ export const SITE = {
|
||||
installCommand: 'npm install -g @gitlawb/openclaude@latest',
|
||||
npmUrl: 'https://www.npmjs.com/package/@gitlawb/openclaude',
|
||||
github: 'https://github.com/Gitlawb/openclaude',
|
||||
releasesUrl: 'https://github.com/Gitlawb/openclaude/releases',
|
||||
gitlawb: 'https://gitlawb.com',
|
||||
gitlawbRepo: 'https://gitlawb.com/node/repos/z6MkqDnb/openclaude',
|
||||
version: latestVersion,
|
||||
ogDefault: '/og/default.png',
|
||||
ogDocs: '/og/docs.png',
|
||||
} as const
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import { SITE } from '../data/site'
|
||||
import { releases, releaseUrl, RELEASES_URL } from '../data/releases'
|
||||
|
||||
const title = `what's new in openclaude — release highlights | openclaude`
|
||||
const description = `Curated highlights from every openclaude release: buddy companions, GPT-5.6 support, new providers, background sessions, and more. Currently at v${SITE.version}.`
|
||||
|
||||
const jsonLd = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: `what's new in openclaude`,
|
||||
url: `${SITE.url}/changelog/`,
|
||||
description,
|
||||
isPartOf: { '@type': 'WebSite', name: SITE.name, url: SITE.url },
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'openclaude', item: SITE.url },
|
||||
{ '@type': 'ListItem', position: 2, name: 'changelog', item: `${SITE.url}/changelog/` },
|
||||
],
|
||||
},
|
||||
]
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description} jsonLd={jsonLd}>
|
||||
<section class="hero grid-bg" aria-labelledby="changelog-heading">
|
||||
<div class="container hero-inner">
|
||||
<p class="pill"><span class="dot" aria-hidden="true"></span>currently v{SITE.version}</p>
|
||||
<h1 id="changelog-heading" class="text-hero">what's new.</h1>
|
||||
<p class="hero-sub">
|
||||
the highlights that matter from each release — hand-picked, not a commit dump.
|
||||
full notes live on <a href={RELEASES_URL} rel="noopener">github releases</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" aria-label="release history">
|
||||
<div class="container">
|
||||
<ol class="timeline">
|
||||
{releases.map(r => (
|
||||
<li class="release">
|
||||
<div class="release-meta">
|
||||
<a class="release-ver" href={releaseUrl(r.version)} rel="noopener">v{r.version}</a>
|
||||
<time datetime={r.date}>{r.date}</time>
|
||||
</div>
|
||||
<div class="release-body">
|
||||
<h2>{r.theme}</h2>
|
||||
<ul>
|
||||
{r.highlights.map(h => <li>{h}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p class="text-body-dim older">
|
||||
looking for something older? the complete history is in the
|
||||
<a href={`${SITE.github}/blob/main/CHANGELOG.md`} rel="noopener"> changelog on github</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding: calc(var(--nav-h) + clamp(3.5rem, 10vh, 6rem)) 0 clamp(3rem, 8vh, 5rem);
|
||||
}
|
||||
.hero-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 1.4rem;
|
||||
}
|
||||
.hero-sub {
|
||||
max-width: 34rem;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: clamp(0.9rem, 1.4vw, 1rem);
|
||||
line-height: 1.7;
|
||||
}
|
||||
.hero-sub a { color: var(--accent); }
|
||||
|
||||
.timeline {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-width: 46rem;
|
||||
}
|
||||
.release {
|
||||
display: grid;
|
||||
grid-template-columns: 7.5rem 1fr;
|
||||
gap: 1.5rem;
|
||||
padding: 1.75rem 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.release:first-child { padding-top: 0; }
|
||||
@media (max-width: 40rem) {
|
||||
.release { grid-template-columns: 1fr; gap: 0.5rem; }
|
||||
}
|
||||
.release-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.release-ver {
|
||||
color: var(--accent);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.release-meta time { color: var(--faint); font-size: 0.72rem; letter-spacing: 0.04em; }
|
||||
.release-body h2 {
|
||||
margin: 0 0 0.75rem;
|
||||
color: var(--ink);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.release-body ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.release-body li {
|
||||
position: relative;
|
||||
padding: 0.25rem 0 0.25rem 1.1rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.release-body li::before {
|
||||
content: '●';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--accent);
|
||||
font-size: 0.5rem;
|
||||
top: 0.72rem;
|
||||
}
|
||||
.older { margin-top: 2rem; font-size: 0.8rem; }
|
||||
.older a { color: var(--accent); }
|
||||
</style>
|
||||
@@ -38,7 +38,7 @@ const toc = [
|
||||
{group.items
|
||||
.filter(item => item.href !== '/docs/')
|
||||
.map(item => (
|
||||
<li><a href={item.href}>{item.title}</a></li>
|
||||
<li><a href={item.href} rel={item.newTab ? 'noopener' : undefined} target={item.newTab ? '_blank' : undefined}>{item.title}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
|
||||
@@ -57,7 +57,6 @@ const jsonLd = [
|
||||
name: 'openclaude',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
operatingSystem: 'macOS, Linux, Windows',
|
||||
softwareVersion: SITE.version,
|
||||
url: SITE.url,
|
||||
downloadUrl: SITE.npmUrl,
|
||||
description: SITE.description,
|
||||
@@ -102,7 +101,7 @@ const jsonLd = [
|
||||
</div>
|
||||
<Terminal>
|
||||
<span class="ln"><span class="t-prompt">$</span> openclaude</span>
|
||||
<span class="ln t-dim">openclaude v{SITE.version} · provider: ollama · model: qwen3-coder</span>
|
||||
<span class="ln t-dim">openclaude · provider: ollama · model: qwen3-coder</span>
|
||||
<span class="ln"> </span>
|
||||
<span class="ln"><span class="t-prompt">></span> add retry with backoff to the fetch client</span>
|
||||
<span class="ln"> </span>
|
||||
|
||||
Reference in New Issue
Block a user