mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix. Complete App configurations produce the same auth options as before. ## What is the current behavior? Without the docs GitHub App private key, two things fail for a contributor: - `pnpm run embeddings` aborts before doing any work. The lint warnings source throws, and every source shares one `Promise.all` in [`fetchAllSources()`](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/sources/index.ts). - `pnpm --filter docs build` exits 1 in prebuild, so the `npm run build` pre-flight CONTRIBUTING.md asks for cannot run either: ``` Error: DOCS_GITHUB_APP_PRIVATE_KEY environment variable is required at octokit (apps/docs/lib/octokit.ts:21:13) at fetchAiSkills (apps/docs/scripts/federated-content/fetch-federated-content.ts:258:36) ``` Both read public content, so this is a rate-limit guard rather than access control: App auth landed in #43015 because unauthenticated calls (60 req/hr per IP) went flaky on shared runners. ## What is the new behavior? `apps/docs/lib/octokit.auth.ts` adds one rung below the App: a token from `GH_TOKEN`, then `GITHUB_TOKEN` (the precedence [`gh help environment`](https://cli.github.com/manual/gh_help_environment) documents), so `export GH_TOKEN=$(gh auth token)` is enough to build locally. Still authenticated, so #43015's fix holds, and still an authenticated Octokit client, so #44274 holds. A partially configured App is now an error naming the missing vars, rather than falling through to a token. Used by the lint warnings loader and `lib/octokit.ts`. The two token vars are declared in `apps/docs/turbo.jsonc` for `turbo/no-undeclared-env-vars`. ## Additional context With only `GH_TOKEN` set, `turbo run build --filter=docs --force` passes 4/4 and search-index source loading completes. `pnpm test` passes (20 files, 164 tests), and `tsc --noEmit` plus `pnpm run lint` match `origin/master`. For a complete App config the auth options are identical to before. Happy to post the fuller verification as a comment. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added flexible GitHub authentication for documentation services, supporting GitHub App credentials or personal access tokens. - GitHub App authentication is preferred when fully configured, with token-based fallback when unavailable. - Added support for both `GH_TOKEN` and `GITHUB_TOKEN`, with clear precedence rules. - **Bug Fixes** - Improved configuration validation with clear errors for missing or incomplete authentication settings. - Standardized authentication across GitHub content and lint-warning retrieval. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
163 lines
3.9 KiB
TypeScript
163 lines
3.9 KiB
TypeScript
import 'server-only'
|
|
|
|
import { Octokit } from '@octokit/core'
|
|
import { retry } from '@octokit/plugin-retry'
|
|
|
|
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
|
import { githubAuthOptions } from './octokit.auth'
|
|
import { OCTOKIT_RETRY_OPTIONS } from './octokit.constants'
|
|
|
|
export { OCTOKIT_RETRY_OPTIONS }
|
|
|
|
const RetryOctokit = Octokit.plugin(retry)
|
|
|
|
let octokitInstance: InstanceType<typeof RetryOctokit>
|
|
|
|
export function octokit() {
|
|
if (!octokitInstance) {
|
|
octokitInstance = new RetryOctokit(githubAuthOptions())
|
|
}
|
|
|
|
return octokitInstance
|
|
}
|
|
|
|
type GithubFileRequest = {
|
|
org: string
|
|
repo: string
|
|
path: string
|
|
branch: string
|
|
options?: {
|
|
onError?: (err?: unknown) => void
|
|
/**
|
|
*
|
|
* A custom fetch implementation to control Next.js caching.
|
|
* By default, uses a "once-per-day" revalidation strategy.
|
|
* This default may change later as we move to on-demand revalidation.
|
|
*/
|
|
fetch?: (info: RequestInfo, init?: RequestInit) => Promise<Response>
|
|
}
|
|
}
|
|
|
|
export async function getGitHubFileContents({
|
|
org,
|
|
repo,
|
|
path,
|
|
branch,
|
|
options: { onError, fetch } = {},
|
|
}: GithubFileRequest) {
|
|
if (path.startsWith('/')) {
|
|
path = path.slice(1)
|
|
}
|
|
|
|
const client = octokit()
|
|
let response: Awaited<
|
|
ReturnType<typeof client.request<'GET /repos/{owner}/{repo}/contents/{path}'>>
|
|
>
|
|
try {
|
|
response = await client.request('GET /repos/{owner}/{repo}/contents/{path}', {
|
|
owner: org,
|
|
repo: repo,
|
|
path: path,
|
|
ref: branch,
|
|
request: OCTOKIT_RETRY_OPTIONS,
|
|
options: {
|
|
fetch: fetch ?? fetchRevalidatePerDay,
|
|
},
|
|
})
|
|
} catch (err) {
|
|
const error = new Error(
|
|
`getGitHubFileContents: request failed for ${org}/${repo}/${path}@${branch}`,
|
|
{ cause: err }
|
|
)
|
|
onError?.(error)
|
|
throw error
|
|
}
|
|
|
|
if (Array.isArray(response.data)) {
|
|
const error = new Error(
|
|
`getGitHubFileContents: ${path} in ${org}/${repo} is a directory, not a file`
|
|
)
|
|
onError?.(error)
|
|
throw error
|
|
}
|
|
if (!('content' in response.data) || response.data.type !== 'file') {
|
|
const error = new Error(
|
|
`getGitHubFileContents: unexpected response for ${path} in ${org}/${repo} (type: ${'type' in response.data ? response.data.type : 'unknown'})`
|
|
)
|
|
onError?.(error)
|
|
throw error
|
|
}
|
|
|
|
return Buffer.from(response.data.content, 'base64').toString('utf-8')
|
|
}
|
|
|
|
export async function getGitHubFileContentsImmutableOnly({
|
|
org,
|
|
repo,
|
|
branch,
|
|
path,
|
|
options: { onError, fetch } = {},
|
|
}: GithubFileRequest): Promise<string> {
|
|
const isImmutableCommit = await checkForImmutableCommit({
|
|
org,
|
|
repo,
|
|
branch,
|
|
})
|
|
if (!isImmutableCommit) {
|
|
throw Error('The commit is not an immutable commit SHA. Tags and branch names are not allowed.')
|
|
}
|
|
|
|
const result = await getGitHubFileContents({
|
|
org,
|
|
repo,
|
|
branch,
|
|
path,
|
|
options: { onError, fetch },
|
|
})
|
|
return result || ''
|
|
}
|
|
|
|
async function checkForImmutableCommit({
|
|
org,
|
|
repo,
|
|
branch,
|
|
options: { fetch: _fetch = fetch } = {},
|
|
}: {
|
|
org: string
|
|
repo: string
|
|
branch: string
|
|
options?: {
|
|
/**
|
|
*
|
|
* A custom fetch implementation to control Next.js caching.
|
|
*/
|
|
fetch?: (info: RequestInfo, init?: RequestInit) => Promise<Response>
|
|
}
|
|
}) {
|
|
try {
|
|
const response = await octokit().request('GET /repos/{owner}/{repo}/git/commits/{commit_sha}', {
|
|
owner: org,
|
|
repo: repo,
|
|
commit_sha: branch,
|
|
headers: {
|
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
},
|
|
request: OCTOKIT_RETRY_OPTIONS,
|
|
options: {
|
|
fetch: _fetch,
|
|
},
|
|
})
|
|
if (response.status === 200) {
|
|
return true
|
|
} else {
|
|
throw Error(
|
|
"Checking for an immutable commit didn't throw an error, but it also didn't return a 200. Erring on the side of safety, assuming this is not an immutable commit.",
|
|
{ cause: response }
|
|
)
|
|
}
|
|
} catch (err) {
|
|
console.error('Not an immutable commit: %o', err)
|
|
return false
|
|
}
|
|
}
|