feat (docs): on-demand revalidation for graphql and wrappers (#35177)

Before:

Revalidation was turned off entirely for wrappers pages, and done daily for GraphQL pages.

After:

Revalidation is via tag. There is an /api/revalidate endpoint that is used by GitHub Actions on the federated repos to trigger the revalidation. This decreases the number of unnecessary page generations by Vercel, and gives the maintainers of federated repos more fine-grained control over when to reploy their docs (see the docs deployment playbook for more info).
This commit is contained in:
Charis
2025-04-24 14:59:38 -04:00
committed by GitHub
parent 9227e83e56
commit 9d9fdcd9fc
5 changed files with 34 additions and 19 deletions

View File

@@ -40,6 +40,9 @@ describe('_handleRevalidateRequest', () => {
process.env.NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:3000'
process.env.SUPABASE_SECRET_KEY = 'secret_key'
// Silence intentional console errors for cleaner test output
vi.spyOn(console, 'error').mockImplementation(() => {})
// Mock current date
mockDate = new Date('2023-01-01T12:00:00Z')
vi.setSystemTime(mockDate)
@@ -109,7 +112,7 @@ describe('_handleRevalidateRequest', () => {
headers: {
Authorization: 'Bearer basic_key',
},
body: JSON.stringify({ tags: ['tag1', 'tag2'] }),
body: JSON.stringify({ tags: ['graphql', 'wrappers'] }),
})
vi.mocked(headers).mockReturnValue(new Headers(request.headers))
@@ -119,8 +122,8 @@ describe('_handleRevalidateRequest', () => {
const response = await _handleRevalidateRequest(request)
expect(response.status).toBe(204)
expect(revalidateTag).toHaveBeenCalledTimes(2)
expect(revalidateTag).toHaveBeenCalledWith('tag1')
expect(revalidateTag).toHaveBeenCalledWith('tag2')
expect(revalidateTag).toHaveBeenCalledWith('graphql')
expect(revalidateTag).toHaveBeenCalledWith('wrappers')
})
it('should return 429 if last revalidation was less than 6 hours ago with basic permissions', async () => {
@@ -129,7 +132,7 @@ describe('_handleRevalidateRequest', () => {
headers: {
Authorization: 'Bearer basic_key',
},
body: JSON.stringify({ tags: ['tag1'] }),
body: JSON.stringify({ tags: ['graphql'] }),
})
vi.mocked(headers).mockReturnValue(new Headers(request.headers))
@@ -150,7 +153,7 @@ describe('_handleRevalidateRequest', () => {
headers: {
Authorization: 'Bearer basic_key',
},
body: JSON.stringify({ tags: ['tag1'] }),
body: JSON.stringify({ tags: ['graphql'] }),
})
vi.mocked(headers).mockReturnValue(new Headers(request.headers))
@@ -162,7 +165,7 @@ describe('_handleRevalidateRequest', () => {
const response = await _handleRevalidateRequest(request)
expect(response.status).toBe(204)
expect(revalidateTag).toHaveBeenCalledWith('tag1')
expect(revalidateTag).toHaveBeenCalledWith('graphql')
})
it('should revalidate regardless of last revalidation time with override permissions', async () => {
@@ -171,7 +174,7 @@ describe('_handleRevalidateRequest', () => {
headers: {
Authorization: 'Bearer override_key',
},
body: JSON.stringify({ tags: ['tag1'] }),
body: JSON.stringify({ tags: ['graphql'] }),
})
vi.mocked(headers).mockReturnValue(new Headers(request.headers))
@@ -183,6 +186,6 @@ describe('_handleRevalidateRequest', () => {
const response = await _handleRevalidateRequest(request)
expect(response.status).toBe(204)
expect(revalidateTag).toHaveBeenCalledWith('tag1')
expect(revalidateTag).toHaveBeenCalledWith('graphql')
})
})

View File

@@ -1,10 +1,10 @@
import { createClient } from '@supabase/supabase-js'
import { type Database } from 'common'
import { revalidateTag } from 'next/cache'
import { headers } from 'next/headers'
import { type NextRequest } from 'next/server'
import { z } from 'zod'
import { type Database } from 'common'
import { VALID_REVALIDATION_TAGS } from '~/features/helpers.fetch'
enum AuthorizationLevel {
Unauthorized,
@@ -13,7 +13,7 @@ enum AuthorizationLevel {
}
const requestBodySchema = z.object({
tags: z.array(z.string()),
tags: z.array(z.enum(VALID_REVALIDATION_TAGS)),
})
export const POST = handleError(_handleRevalidateRequest)
@@ -35,14 +35,12 @@ export async function _handleRevalidateRequest(request: NextRequest) {
}
let authorizationLevel = AuthorizationLevel.Unauthorized
const token = authorization.replace(/^Bearer\s+/, '')
const token = authorization.replace(/^Bearer /, '')
if (overrideKeys.includes(token)) {
authorizationLevel = AuthorizationLevel.Override
} else if (basicKeys.includes(token)) {
authorizationLevel = AuthorizationLevel.Basic
}
if (authorizationLevel === AuthorizationLevel.Unauthorized) {
return new Response('Invalid Authorization header', { status: 401 })
}

View File

@@ -11,12 +11,12 @@ import {
removeRedundantH1,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
export const dynamicParams = false
@@ -196,7 +196,9 @@ const getContent = async (params: Params) => {
const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`
const response = await fetchRevalidatePerDay(`https://raw.githubusercontent.com/${repoPath}`)
const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, {
next: { tags: [REVALIDATION_TAGS.WRAPPERS] },
})
const rawContent = await response.text()
const { content: contentWithoutFrontmatter } = matter(rawContent)

View File

@@ -5,11 +5,11 @@ import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay_TEMP_TESTING } from '~/features/helpers.fetch'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
export const dynamicParams = false
@@ -135,8 +135,9 @@ const getContent = async ({ slug }: Params) => {
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
const response = await fetchRevalidatePerDay_TEMP_TESTING(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
const response = await fetch(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`,
{ next: { tags: [REVALIDATION_TAGS.GRAPHQL] } }
)
const content = await response.text()

View File

@@ -8,6 +8,17 @@
import { ONE_DAY_IN_SECONDS } from './helpers.time'
export const REVALIDATION_TAGS = {
GRAPHQL: 'graphql',
WRAPPERS: 'wrappers',
} as const
// Casting to avoid problems with using this as a Zod enum, TypeScript does
// not recognize the casted type as a supertype of the original type
export const VALID_REVALIDATION_TAGS = Object.values(REVALIDATION_TAGS) as unknown as readonly [
string,
...string[],
]
function fetchWithNextOptions({
next,
cache,