Spaces:
Sleeping
Sleeping
Brixyy commited on
fix: unify GitHub sync token resolution with integrations env file
Browse files- src/app/api/github/route.ts +3 -3
- src/lib/__tests__/runtime-env.test.ts +50 -0
- src/lib/github.ts +6 -4
- src/lib/runtime-env.ts +47 -0
src/app/api/github/route.ts
CHANGED
|
@@ -41,7 +41,7 @@ export async function GET(request: NextRequest) {
|
|
| 41 |
return NextResponse.json({ error: 'repo query parameter required (owner/repo format)' }, { status: 400 })
|
| 42 |
}
|
| 43 |
|
| 44 |
-
const token = getGitHubToken()
|
| 45 |
if (!token) {
|
| 46 |
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
|
| 47 |
}
|
|
@@ -109,7 +109,7 @@ async function handleSync(
|
|
| 109 |
return NextResponse.json({ error: 'repo is required' }, { status: 400 })
|
| 110 |
}
|
| 111 |
|
| 112 |
-
const token = getGitHubToken()
|
| 113 |
if (!token) {
|
| 114 |
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
|
| 115 |
}
|
|
@@ -344,7 +344,7 @@ function handleStatus(workspaceId: number) {
|
|
| 344 |
// ── Stats: GitHub user profile + repo overview ──────────────────
|
| 345 |
|
| 346 |
async function handleGitHubStats() {
|
| 347 |
-
const token = getGitHubToken()
|
| 348 |
if (!token) {
|
| 349 |
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
|
| 350 |
}
|
|
|
|
| 41 |
return NextResponse.json({ error: 'repo query parameter required (owner/repo format)' }, { status: 400 })
|
| 42 |
}
|
| 43 |
|
| 44 |
+
const token = await getGitHubToken()
|
| 45 |
if (!token) {
|
| 46 |
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
|
| 47 |
}
|
|
|
|
| 109 |
return NextResponse.json({ error: 'repo is required' }, { status: 400 })
|
| 110 |
}
|
| 111 |
|
| 112 |
+
const token = await getGitHubToken()
|
| 113 |
if (!token) {
|
| 114 |
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
|
| 115 |
}
|
|
|
|
| 344 |
// ── Stats: GitHub user profile + repo overview ──────────────────
|
| 345 |
|
| 346 |
async function handleGitHubStats() {
|
| 347 |
+
const token = await getGitHubToken()
|
| 348 |
if (!token) {
|
| 349 |
return NextResponse.json({ error: 'GITHUB_TOKEN not configured' }, { status: 400 })
|
| 350 |
}
|
src/lib/__tests__/runtime-env.test.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { afterEach, describe, expect, it } from 'vitest'
|
| 2 |
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
| 3 |
+
import os from 'node:os'
|
| 4 |
+
import path from 'node:path'
|
| 5 |
+
|
| 6 |
+
import { getEffectiveEnvValue } from '../runtime-env'
|
| 7 |
+
|
| 8 |
+
describe('getEffectiveEnvValue', () => {
|
| 9 |
+
afterEach(() => {
|
| 10 |
+
delete process.env.TEST_RUNTIME_ENV
|
| 11 |
+
})
|
| 12 |
+
|
| 13 |
+
it('reads values from the OpenClaw env file before process.env', async () => {
|
| 14 |
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mc-runtime-env-'))
|
| 15 |
+
try {
|
| 16 |
+
const envFilePath = path.join(tmpDir, '.env')
|
| 17 |
+
await writeFile(envFilePath, 'TEST_RUNTIME_ENV=from-file\n', 'utf-8')
|
| 18 |
+
process.env.TEST_RUNTIME_ENV = 'from-process'
|
| 19 |
+
|
| 20 |
+
await expect(getEffectiveEnvValue('TEST_RUNTIME_ENV', { envFilePath })).resolves.toBe('from-file')
|
| 21 |
+
} finally {
|
| 22 |
+
await rm(tmpDir, { recursive: true, force: true })
|
| 23 |
+
}
|
| 24 |
+
})
|
| 25 |
+
|
| 26 |
+
it('falls back to process.env when the env file does not define the key', async () => {
|
| 27 |
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mc-runtime-env-'))
|
| 28 |
+
try {
|
| 29 |
+
const envFilePath = path.join(tmpDir, '.env')
|
| 30 |
+
await writeFile(envFilePath, 'OTHER_KEY=value\n', 'utf-8')
|
| 31 |
+
process.env.TEST_RUNTIME_ENV = 'from-process'
|
| 32 |
+
|
| 33 |
+
await expect(getEffectiveEnvValue('TEST_RUNTIME_ENV', { envFilePath })).resolves.toBe('from-process')
|
| 34 |
+
} finally {
|
| 35 |
+
await rm(tmpDir, { recursive: true, force: true })
|
| 36 |
+
}
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
it('returns an empty string when the key is missing everywhere', async () => {
|
| 40 |
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'mc-runtime-env-'))
|
| 41 |
+
try {
|
| 42 |
+
const envFilePath = path.join(tmpDir, '.env')
|
| 43 |
+
await writeFile(envFilePath, '', 'utf-8')
|
| 44 |
+
|
| 45 |
+
await expect(getEffectiveEnvValue('TEST_RUNTIME_ENV', { envFilePath })).resolves.toBe('')
|
| 46 |
+
} finally {
|
| 47 |
+
await rm(tmpDir, { recursive: true, force: true })
|
| 48 |
+
}
|
| 49 |
+
})
|
| 50 |
+
})
|
src/lib/github.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
/**
|
| 2 |
* GitHub API client for Mission Control issue sync.
|
| 3 |
-
*
|
|
|
|
| 4 |
*/
|
|
|
|
| 5 |
|
| 6 |
export interface GitHubLabel {
|
| 7 |
name: string
|
|
@@ -25,8 +27,8 @@ export interface GitHubIssue {
|
|
| 25 |
updated_at: string
|
| 26 |
}
|
| 27 |
|
| 28 |
-
export function getGitHubToken(): string | null {
|
| 29 |
-
return
|
| 30 |
}
|
| 31 |
|
| 32 |
/**
|
|
@@ -36,7 +38,7 @@ export async function githubFetch(
|
|
| 36 |
path: string,
|
| 37 |
options: RequestInit = {}
|
| 38 |
): Promise<Response> {
|
| 39 |
-
const token = getGitHubToken()
|
| 40 |
if (!token) {
|
| 41 |
throw new Error('GITHUB_TOKEN not configured')
|
| 42 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
* GitHub API client for Mission Control issue sync.
|
| 3 |
+
* Resolves GITHUB_TOKEN from the OpenClaw integration env file first,
|
| 4 |
+
* then falls back to process.env for deployments that export it directly.
|
| 5 |
*/
|
| 6 |
+
import { getEffectiveEnvValue } from '@/lib/runtime-env'
|
| 7 |
|
| 8 |
export interface GitHubLabel {
|
| 9 |
name: string
|
|
|
|
| 27 |
updated_at: string
|
| 28 |
}
|
| 29 |
|
| 30 |
+
export async function getGitHubToken(): Promise<string | null> {
|
| 31 |
+
return await getEffectiveEnvValue('GITHUB_TOKEN') || null
|
| 32 |
}
|
| 33 |
|
| 34 |
/**
|
|
|
|
| 38 |
path: string,
|
| 39 |
options: RequestInit = {}
|
| 40 |
): Promise<Response> {
|
| 41 |
+
const token = await getGitHubToken()
|
| 42 |
if (!token) {
|
| 43 |
throw new Error('GITHUB_TOKEN not configured')
|
| 44 |
}
|
src/lib/runtime-env.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { readFile } from 'node:fs/promises'
|
| 2 |
+
import { join } from 'node:path'
|
| 3 |
+
|
| 4 |
+
import { config } from '@/lib/config'
|
| 5 |
+
|
| 6 |
+
function parseEnvLine(line: string): { key: string; value: string } | null {
|
| 7 |
+
const trimmed = line.trim()
|
| 8 |
+
if (!trimmed || trimmed.startsWith('#')) return null
|
| 9 |
+
|
| 10 |
+
const eqIdx = line.indexOf('=')
|
| 11 |
+
if (eqIdx <= 0) return null
|
| 12 |
+
|
| 13 |
+
const key = line.slice(0, eqIdx).trim()
|
| 14 |
+
const value = line.slice(eqIdx + 1).trim()
|
| 15 |
+
if (!key) return null
|
| 16 |
+
return { key, value }
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
async function readOpenClawEnvFile(envFilePath: string): Promise<Map<string, string>> {
|
| 20 |
+
try {
|
| 21 |
+
const raw = await readFile(envFilePath, 'utf-8')
|
| 22 |
+
const envMap = new Map<string, string>()
|
| 23 |
+
for (const line of raw.split('\n')) {
|
| 24 |
+
const parsed = parseEnvLine(line)
|
| 25 |
+
if (parsed) envMap.set(parsed.key, parsed.value)
|
| 26 |
+
}
|
| 27 |
+
return envMap
|
| 28 |
+
} catch (error: any) {
|
| 29 |
+
if (error?.code === 'ENOENT') return new Map<string, string>()
|
| 30 |
+
throw error
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export async function getEffectiveEnvValue(
|
| 35 |
+
key: string,
|
| 36 |
+
options?: { envFilePath?: string }
|
| 37 |
+
): Promise<string> {
|
| 38 |
+
const envFilePath = options?.envFilePath || join(config.openclawStateDir, '.env')
|
| 39 |
+
const envMap = await readOpenClawEnvFile(envFilePath)
|
| 40 |
+
const fromFile = envMap.get(key)
|
| 41 |
+
if (typeof fromFile === 'string' && fromFile.length > 0) return fromFile
|
| 42 |
+
|
| 43 |
+
const fromProcess = process.env[key]
|
| 44 |
+
if (typeof fromProcess === 'string' && fromProcess.length > 0) return fromProcess
|
| 45 |
+
|
| 46 |
+
return ''
|
| 47 |
+
}
|