Files
supabase/apps/docs/lib/octokit.auth.test.ts
Barry Roodt f89c362b26 fix(docs): accept a GitHub token for docs content reads (#48364)
## 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 -->
2026-08-13 16:26:15 +00:00

89 lines
3.2 KiB
TypeScript

import crypto from 'node:crypto'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { githubAuthOptions } from './octokit.auth.js'
const APP_ID = '123456'
const INSTALLATION_ID = '7890'
// PKCS1 on purpose: the App rung has to convert it, and universal-github-app-jwt
// only accepts PKCS8.
const PKCS1_PRIVATE_KEY = crypto
.generateKeyPairSync('rsa', { modulusLength: 2048 })
.privateKey.export({ type: 'pkcs1', format: 'pem' })
.toString()
function stubEnv(env: Record<string, string>) {
// Empty string rather than `undefined`: some Vitest versions stringify the
// latter to 'undefined', which is truthy and would silently defeat these tests.
for (const name of [
'DOCS_GITHUB_APP_ID',
'DOCS_GITHUB_APP_INSTALLATION_ID',
'DOCS_GITHUB_APP_PRIVATE_KEY',
'GH_TOKEN',
'GITHUB_TOKEN',
]) {
vi.stubEnv(name, env[name] ?? '')
}
}
const APP_ENV = {
DOCS_GITHUB_APP_ID: APP_ID,
DOCS_GITHUB_APP_INSTALLATION_ID: INSTALLATION_ID,
DOCS_GITHUB_APP_PRIVATE_KEY: PKCS1_PRIVATE_KEY,
}
describe('githubAuthOptions', () => {
afterEach(() => vi.unstubAllEnvs())
it('authenticates as the App and converts the key to PKCS8', () => {
stubEnv(APP_ENV)
const options = githubAuthOptions()
if (!('authStrategy' in options)) throw new Error('expected App auth')
expect(options.auth).toMatchObject({ appId: APP_ID, installationId: INSTALLATION_ID })
expect(options.auth.privateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/)
})
it('prefers the App when a token is also present', () => {
stubEnv({ ...APP_ENV, GITHUB_TOKEN: 'ghp_example' })
expect(githubAuthOptions()).toHaveProperty('authStrategy')
})
it.each(['GH_TOKEN', 'GITHUB_TOKEN'])(
'authenticates with a token from %s when the App is not configured',
(name) => {
stubEnv({ [name]: 'ghp_example' })
expect(githubAuthOptions()).toEqual({ auth: 'ghp_example' })
}
)
it('gives GH_TOKEN precedence over GITHUB_TOKEN, as the gh CLI documents', () => {
stubEnv({ GH_TOKEN: 'ghp_from_gh', GITHUB_TOKEN: 'ghp_from_actions' })
expect(githubAuthOptions()).toEqual({ auth: 'ghp_from_gh' })
})
it('refuses a partially configured App instead of masking it with a token', () => {
stubEnv({ DOCS_GITHUB_APP_ID: APP_ID, GITHUB_TOKEN: 'ghp_example' })
expect(githubAuthOptions).toThrow(/Incomplete GitHub App configuration/)
// Names what is missing, and not what is already set.
expect(githubAuthOptions).toThrow(/DOCS_GITHUB_APP_INSTALLATION_ID/)
expect(githubAuthOptions).toThrow(/DOCS_GITHUB_APP_PRIVATE_KEY/)
expect(githubAuthOptions).not.toThrow(/DOCS_GITHUB_APP_ID\b/)
})
it('reports only the missing App var when one is absent', () => {
stubEnv({
DOCS_GITHUB_APP_ID: APP_ID,
DOCS_GITHUB_APP_INSTALLATION_ID: INSTALLATION_ID,
GITHUB_TOKEN: 'ghp_example',
})
expect(githubAuthOptions).toThrow(/DOCS_GITHUB_APP_PRIVATE_KEY not set\. Set all three/)
})
it('names every credential option when none is set', () => {
stubEnv({})
expect(githubAuthOptions).toThrow(/DOCS_GITHUB_APP_ID/)
expect(githubAuthOptions).toThrow(/GH_TOKEN/)
expect(githubAuthOptions).toThrow(/GITHUB_TOKEN/)
})
})