Brixyy commited on
Commit
28d83bd
·
unverified ·
1 Parent(s): d91ef14

fix: unify GitHub sync token resolution with integrations env file

Browse files
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
- * Uses GITHUB_TOKEN from env (integration key, not core config).
 
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 process.env.GITHUB_TOKEN || null
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
+ }