-
Notifications
You must be signed in to change notification settings - Fork 2
Extended integration test playground with real CodeQL databases #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,258 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Download CodeQL databases using the GitHub REST API. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * When running inside the VS Code Extension Development Host, this uses | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the VS Code GitHub authentication session (same auth as vscode-codeql). | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * When running standalone, it falls back to the GH_TOKEN env var. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Downloads are cached on disk and reused if less than 24 hours old. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'fs'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { join } from 'path'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { execFileSync } from 'child_process'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { homedir } from 'os'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { pipeline } from 'stream/promises'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface RepoConfig { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| callGraphFromTo?: { sourceFunction: string; targetFunction: string }; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| language: string; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| owner: string; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| repo: string; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Get a GitHub token. Tries VS Code auth session first, then GH_TOKEN env var, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * then `gh auth token` CLI. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function getGitHubToken(): Promise<string | undefined> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Try VS Code authentication (when running in Extension Host) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const vscode = await import('vscode'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const session = await vscode.authentication.getSession('github', ['repo'], { createIfNone: false }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (session?.accessToken) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(' 🔑 Using VS Code GitHub authentication'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return session.accessToken; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Not in VS Code — fall through | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Try GH_TOKEN env var | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (process.env.GH_TOKEN) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(' 🔑 Using GH_TOKEN environment variable'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return process.env.GH_TOKEN; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Try `gh auth token` CLI | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { execFileSync } = await import('child_process'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = execFileSync('gh', ['auth', 'token'], { encoding: 'utf8', timeout: 5000 }).trim(); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (token) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(' 🔑 Using GitHub CLI (gh auth token)'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return token; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // gh CLI not available or not authenticated | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Download a CodeQL database for a repository via GitHub REST API. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns the path to the extracted database, or null if download failed. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function downloadDatabase( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| repo: RepoConfig, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| databaseDir: string, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| token: string, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<string | null> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { language, owner, repo: repoName } = repo; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const repoDir = join(databaseDir, owner, repoName); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dbDir = join(repoDir, language); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const zipPath = join(repoDir, `${language}.zip`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const markerFile = join(dbDir, 'codeql-database.yml'); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check cache | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (existsSync(markerFile)) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mtime = statSync(markerFile).mtimeMs; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Date.now() - mtime < MAX_AGE_MS) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` ✓ Cached: ${owner}/${repoName} (${language})`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return dbDir; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fall through to download | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mkdirSync(repoDir, { recursive: true }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/code-scanning/codeql/databases/${encodeURIComponent(language)}`; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` ⬇ Downloading: ${owner}/${repoName} (${language})...`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch(url, { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warningCode scanning / CodeQL File data in outbound network request Medium test
Outbound network request depends on
file data Error loading related location Loading Outbound network request depends on file data Error loading related location Loading |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Accept: 'application/zip', | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${token}`, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'User-Agent': 'codeql-development-mcp-server-extended-tests', | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!response.ok) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(` ✗ Download failed: ${response.status} ${response.statusText}`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!response.body) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(` ✗ Empty response body`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Stream to zip file | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dest = createWriteStream(zipPath); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-expect-error — ReadableStream → NodeJS.ReadableStream interop | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await pipeline(response.body, dest); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+116
to
+120
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` 📦 Extracting: ${owner}/${repoName} (${language})...`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mkdirSync(dbDir, { recursive: true }); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| execFileSync('unzip', ['-o', '-q', zipPath, '-d', dbDir]); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+122
to
+125
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Flatten if single nested directory (zip often has one top-level dir) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entries = readdirSync(dbDir); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entries.length === 1 && !existsSync(join(dbDir, 'codeql-database.yml'))) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nested = join(dbDir, entries[0]); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (existsSync(join(nested, 'codeql-database.yml'))) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Copy all contents up, then remove the nested directory | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| execFileSync('bash', ['-c', `cp -a "${nested}"/. "${dbDir}/" && rm -rf "${nested}"`]); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warningCode scanning / CodeQL Shell command built from environment values Medium test
This shell command depends on an uncontrolled
absolute path Error loading related location Loading This shell command depends on an uncontrolled absolute path Error loading related location Loading This shell command depends on an uncontrolled absolute path Error loading related location Loading
Copilot AutofixAI 1 day ago In general, to fix this kind of problem you should avoid passing dynamically constructed command strings to a shell ( For this specific case, the shell is only being used to run The best minimal-impact fix is:
Concretely:
No changes are required in
Suggested changeset
1
extensions/vscode/test/extended/download-databases.ts
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+129
to
+133
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!existsSync(markerFile)) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(` ✗ Extraction failed: ${markerFile} not found`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clean up zip | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { const { unlinkSync } = await import('fs'); unlinkSync(zipPath); } catch { /* best effort */ } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` ✓ Ready: ${owner}/${repoName} (${language})`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return dbDir; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(` ✗ Error downloading ${owner}/${repoName}: ${err}`); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Get the default vscode-codeql global storage paths (platform-dependent). | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getVscodeCodeqlStoragePaths(): string[] { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const home = homedir(); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const candidates = [ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| join(home, '.config', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| join(home, '.config', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'), | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+157
to
+162
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const candidates = [ | |
| join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'), | |
| join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'), | |
| join(home, '.config', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'), | |
| join(home, '.config', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'), | |
| ]; | |
| const candidates = [ | |
| // macOS | |
| join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'), | |
| join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'), | |
| // Linux | |
| join(home, '.config', 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'), | |
| join(home, '.config', 'Code', 'User', 'globalStorage', 'github.vscode-codeql'), | |
| ]; | |
| // Windows: %APPDATA%\Code\User\globalStorage\GitHub.vscode-codeql | |
| if (process.platform === 'win32' && process.env.APPDATA) { | |
| candidates.push( | |
| join(process.env.APPDATA, 'Code', 'User', 'globalStorage', 'GitHub.vscode-codeql'), | |
| join(process.env.APPDATA, 'Code', 'User', 'globalStorage', 'github.vscode-codeql'), | |
| ); | |
| } |
Copilot
AI
Mar 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This splits CODEQL_DATABASES_BASE_DIRS on ':' but the server expects a platform-delimited list (path.delimiter). Use path.delimiter here to support Windows (';') and keep behavior consistent with server discovery-config parsing.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| { | ||
| "description": "Repositories for extended MCP integration testing with CallGraphFromTo source/target function pairs.", | ||
| "repositories": [ | ||
| { | ||
| "owner": "gin-gonic", | ||
| "repo": "gin", | ||
| "language": "go", | ||
| "callGraphFromTo": { "sourceFunction": "handleHTTPRequest", "targetFunction": "ServeHTTP" } | ||
| }, | ||
| { | ||
| "owner": "expressjs", | ||
| "repo": "express", | ||
| "language": "javascript", | ||
| "callGraphFromTo": { "sourceFunction": "json", "targetFunction": "send" } | ||
| }, | ||
| { | ||
| "owner": "checkstyle", | ||
| "repo": "checkstyle", | ||
| "language": "java", | ||
| "callGraphFromTo": { "sourceFunction": "process", "targetFunction": "log" } | ||
| }, | ||
| { | ||
| "owner": "PyCQA", | ||
| "repo": "flake8", | ||
| "language": "python", | ||
| "callGraphFromTo": { "sourceFunction": "run", "targetFunction": "report" } | ||
| } | ||
| ], | ||
| "settings": { | ||
| "databaseDir": ".tmp/extended-test-databases", | ||
| "fixtureSearchDirs": [ | ||
| "test/fixtures/single-folder-workspace/codeql-storage/databases", | ||
| "test/fixtures/multi-root-workspace/folder-a/codeql-storage/databases" | ||
| ], | ||
| "timeoutMs": 600000 | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extendedTestConfig sets
external: [], but the extended test code dynamically imports 'vscode'. With no externalization, esbuild will try to resolve/bundle 'vscode' and the build will fail (since it's provided by the extension host, not npm). Keep 'vscode' external for this target so the optional import can be attempted and caught at runtime.