fix: unify GitHub sync token resolution with integrations env file

This commit is contained in:
Brixyy 2026-03-14 14:46:29 +01:00 committed by GitHub
parent 8386cfbec0
commit e2ddf09a72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 106 additions and 7 deletions

View File

@ -41,7 +41,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'repo query parameter required (owner/repo format)' }, { status: 400 })
}
const token = getGitHubToken()
const token = await getGitHubToken()
if (!token) {
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
}
@ -109,7 +109,7 @@ async function handleSync(
return NextResponse.json({ error: 'repo is required' }, { status: 400 })
}
const token = getGitHubToken()
const token = await getGitHubToken()
if (!token) {
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
}
@ -344,7 +344,7 @@ function handleStatus(workspaceId: number) {
// ── Stats: GitHub user profile + repo overview ──────────────────
async function handleGitHubStats() {
const token = getGitHubToken()
const token = await getGitHubToken()
if (!token) {
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
}

View File

@ -0,0 +1,50 @@
import { afterEach, describe, expect, it } from 'vitest'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { getEffectiveEnvValue } from '../runtime-env'
describe('getEffectiveEnvValue', () => {
afterEach(() => {
delete process.env.TEST_RUNTIME_ENV
})
it('reads values from the OpenClaw env file before process.env', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mc-runtime-env-'))
try {
const envFilePath = path.join(tmpDir, '.env')
await writeFile(envFilePath, 'TEST_RUNTIME_ENV=from-file\n', 'utf-8')
process.env.TEST_RUNTIME_ENV = 'from-process'
await expect(getEffectiveEnvValue('TEST_RUNTIME_ENV', { envFilePath })).resolves.toBe('from-file')
} finally {
await rm(tmpDir, { recursive: true, force: true })
}
})
it('falls back to process.env when the env file does not define the key', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mc-runtime-env-'))
try {
const envFilePath = path.join(tmpDir, '.env')
await writeFile(envFilePath, 'OTHER_KEY=value\n', 'utf-8')
process.env.TEST_RUNTIME_ENV = 'from-process'
await expect(getEffectiveEnvValue('TEST_RUNTIME_ENV', { envFilePath })).resolves.toBe('from-process')
} finally {
await rm(tmpDir, { recursive: true, force: true })
}
})
it('returns an empty string when the key is missing everywhere', async () => {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mc-runtime-env-'))
try {
const envFilePath = path.join(tmpDir, '.env')
await writeFile(envFilePath, '', 'utf-8')
await expect(getEffectiveEnvValue('TEST_RUNTIME_ENV', { envFilePath })).resolves.toBe('')
} finally {
await rm(tmpDir, { recursive: true, force: true })
}
})
})

View File

@ -1,7 +1,9 @@
/**
* GitHub API client for Mission Control issue sync.
* Uses GITHUB_TOKEN from env (integration key, not core config).
* Resolves GITHUB_TOKEN from the OpenClaw integration env file first,
* then falls back to process.env for deployments that export it directly.
*/
import { getEffectiveEnvValue } from '@/lib/runtime-env'
export interface GitHubLabel {
name: string
@ -25,8 +27,8 @@ export interface GitHubIssue {
updated_at: string
}
export function getGitHubToken(): string | null {
return process.env.GITHUB_TOKEN || null
export async function getGitHubToken(): Promise<string | null> {
return await getEffectiveEnvValue('GITHUB_TOKEN') || null
}
/**
@ -36,7 +38,7 @@ export async function githubFetch(
path: string,
options: RequestInit = {}
): Promise<Response> {
const token = getGitHubToken()
const token = await getGitHubToken()
if (!token) {
throw new Error('GITHUB_TOKEN not configured')
}

47
src/lib/runtime-env.ts Normal file
View File

@ -0,0 +1,47 @@
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { config } from '@/lib/config'
function parseEnvLine(line: string): { key: string; value: string } | null {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return null
const eqIdx = line.indexOf('=')
if (eqIdx <= 0) return null
const key = line.slice(0, eqIdx).trim()
const value = line.slice(eqIdx + 1).trim()
if (!key) return null
return { key, value }
}
async function readOpenClawEnvFile(envFilePath: string): Promise<Map<string, string>> {
try {
const raw = await readFile(envFilePath, 'utf-8')
const envMap = new Map<string, string>()
for (const line of raw.split('\n')) {
const parsed = parseEnvLine(line)
if (parsed) envMap.set(parsed.key, parsed.value)
}
return envMap
} catch (error: any) {
if (error?.code === 'ENOENT') return new Map<string, string>()
throw error
}
}
export async function getEffectiveEnvValue(
key: string,
options?: { envFilePath?: string }
): Promise<string> {
const envFilePath = options?.envFilePath || join(config.openclawStateDir, '.env')
const envMap = await readOpenClawEnvFile(envFilePath)
const fromFile = envMap.get(key)
if (typeof fromFile === 'string' && fromFile.length > 0) return fromFile
const fromProcess = process.env[key]
if (typeof fromProcess === 'string' && fromProcess.length > 0) return fromProcess
return ''
}