Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@resvg/resvg-js": "^2.6.2",
"@vercel/analytics": "^2.0.1",
"@vercel/functions": "^3.7.6",
"@vercel/global-config": "^1.5.1",
"@vercel/otel": "^2.1.3",
"@vercel/speed-insights": "^2.0.0",
"@vueuse/core": "^14.3.0",
Expand Down
26 changes: 26 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 17 additions & 4 deletions server/utils/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,28 @@ export function getHeadRef(): string {
return headRef
}

/**
* The SHA production should currently serve:
* - global config pin if one is set (production only)
* - latest commit touching the content directory via `resolveContentSha()`
*/
export async function resolveProdSha(): Promise<string> {
const { contentDir } = useRuntimeConfig().docs
if (process.env.VERCEL_ENV === 'production') {
const pinned = await getPinnedSha()
if (pinned) return pinned
}
return resolveContentSha(targetBranch(), contentDir)
}

/**
* Shared content instance for the lifetime of this server instance, pinned to `headRef`. In production every
* call resolves the latest commit touching the content directory via `resolveContentSha()` — a shared,
* short-TTL cache, not a per-instance timer — and rebuilds when that advances. Previews stay pinned.
* call resolves the current head via `resolveProdSha()` — a shared, short-TTL cache, not a per-instance
* timer — and rebuilds when that advances. Previews stay pinned.
*/
export async function getProdContent(): Promise<ComarkContent> {
if (['production', 'preview'].includes(process.env.VERCEL_ENV || '')) {
const { contentDir } = useRuntimeConfig().docs
const sha = await resolveContentSha(targetBranch(), contentDir)
const sha = await resolveProdSha()
if (sha !== getHeadRef()) {
console.log(`[content] head ${getHeadRef()} -> ${sha}`)
headRef = sha
Expand Down
18 changes: 18 additions & 0 deletions server/utils/global-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { get } from '@vercel/global-config'

/** Key looked up in the connected Global Config store for the pinned production content SHA. */
const PINNED_SHA_KEY = 'contentSha'

/**
* The content SHA pinned in Vercel Global Config, if any.
*/
export async function getPinnedSha(): Promise<string | undefined> {
if (!process.env.GLOBAL_CONFIG) return undefined
try {
const sha = await get<string>(PINNED_SHA_KEY)
return sha || undefined
} catch (error) {
console.error('[content] failed to read pinned SHA from Global Config', error)
return undefined
}
}
44 changes: 44 additions & 0 deletions test/global-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { afterEach, describe, expect, it, vi } from 'vitest'

const { get } = vi.hoisted(() => ({ get: vi.fn() }))
vi.mock('@vercel/global-config', () => ({ get }))

afterEach(() => {
vi.unstubAllEnvs()
get.mockReset()
})

describe('getPinnedSha', () => {
it('returns undefined without reading the store when no Global Config is connected', async () => {
vi.stubEnv('GLOBAL_CONFIG', '')
const { getPinnedSha } = await import('../server/utils/global-config')

await expect(getPinnedSha()).resolves.toBeUndefined()
expect(get).not.toHaveBeenCalled()
})

it('returns the pinned SHA when the store has one', async () => {
vi.stubEnv('GLOBAL_CONFIG', 'connection-string')
get.mockResolvedValue('abc123')
const { getPinnedSha } = await import('../server/utils/global-config')

await expect(getPinnedSha()).resolves.toBe('abc123')
expect(get).toHaveBeenCalledWith('contentSha')
})

it('returns undefined when the key is unset', async () => {
vi.stubEnv('GLOBAL_CONFIG', 'connection-string')
get.mockResolvedValue(undefined)
const { getPinnedSha } = await import('../server/utils/global-config')

await expect(getPinnedSha()).resolves.toBeUndefined()
})

it('falls back to undefined when the read fails', async () => {
vi.stubEnv('GLOBAL_CONFIG', 'connection-string')
get.mockRejectedValue(new Error('network error'))
const { getPinnedSha } = await import('../server/utils/global-config')

await expect(getPinnedSha()).resolves.toBeUndefined()
})
})
Loading