GitHub workflows (#428)
Fixes #348 Fixes #274 Fixes #149 - Connect to existing repos - Push to other branches on GitHub besides main - Allows force push (with confirmation) dialog --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
417
testing/fake-llm-server/githubHandler.ts
Normal file
417
testing/fake-llm-server/githubHandler.ts
Normal file
@@ -0,0 +1,417 @@
|
||||
import { Request, Response } from "express";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
|
||||
const gitHttpMiddlewareFactory = require("git-http-mock-server/middleware");
|
||||
|
||||
// Push event tracking for tests
|
||||
interface PushEvent {
|
||||
timestamp: Date;
|
||||
repo: string;
|
||||
branch: string;
|
||||
operation: "push" | "create" | "delete";
|
||||
commitSha?: string;
|
||||
}
|
||||
|
||||
const pushEvents: PushEvent[] = [];
|
||||
|
||||
// Mock data for testing
|
||||
const mockAccessToken = "fake_access_token_12345";
|
||||
const mockDeviceCode = "fake_device_code_12345";
|
||||
const mockUserCode = "FAKE-CODE";
|
||||
const mockUser = {
|
||||
login: "testuser",
|
||||
id: 12345,
|
||||
email: "testuser@example.com",
|
||||
};
|
||||
|
||||
const mockRepos = [
|
||||
{
|
||||
id: 1,
|
||||
name: "test-repo-1",
|
||||
full_name: "testuser/test-repo-1",
|
||||
private: false,
|
||||
owner: { login: "testuser" },
|
||||
default_branch: "main",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "test-repo-2",
|
||||
full_name: "testuser/test-repo-2",
|
||||
private: true,
|
||||
owner: { login: "testuser" },
|
||||
default_branch: "main",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "existing-app",
|
||||
full_name: "testuser/existing-app",
|
||||
private: false,
|
||||
owner: { login: "testuser" },
|
||||
default_branch: "main",
|
||||
},
|
||||
];
|
||||
|
||||
const mockBranches = [
|
||||
{ name: "main", commit: { sha: "abc123" } },
|
||||
{ name: "develop", commit: { sha: "def456" } },
|
||||
{ name: "feature/test", commit: { sha: "ghi789" } },
|
||||
];
|
||||
|
||||
// Store device flow state
|
||||
let deviceFlowState = {
|
||||
deviceCode: mockDeviceCode,
|
||||
userCode: mockUserCode,
|
||||
authorized: false,
|
||||
pollCount: 0,
|
||||
};
|
||||
|
||||
// GitHub Device Flow - Step 1: Get device code
|
||||
export function handleDeviceCode(req: Request, res: Response) {
|
||||
console.log("* GitHub Device Code requested");
|
||||
|
||||
// Reset state for new flow
|
||||
deviceFlowState = {
|
||||
deviceCode: mockDeviceCode,
|
||||
userCode: mockUserCode,
|
||||
authorized: false,
|
||||
pollCount: 0,
|
||||
};
|
||||
|
||||
res.json({
|
||||
device_code: mockDeviceCode,
|
||||
user_code: mockUserCode,
|
||||
verification_uri: "https://github.com/login/device",
|
||||
verification_uri_complete: `https://github.com/login/device?user_code=${mockUserCode}`,
|
||||
expires_in: 900,
|
||||
interval: 1, // Short interval for testing
|
||||
});
|
||||
}
|
||||
|
||||
// GitHub Device Flow - Step 2: Poll for access token
|
||||
export function handleAccessToken(req: Request, res: Response) {
|
||||
console.log("* GitHub Access Token polling", {
|
||||
pollCount: deviceFlowState.pollCount,
|
||||
});
|
||||
|
||||
const { device_code } = req.body;
|
||||
|
||||
if (device_code !== mockDeviceCode) {
|
||||
return res.status(400).json({
|
||||
error: "invalid_request",
|
||||
error_description: "Invalid device code",
|
||||
});
|
||||
}
|
||||
|
||||
deviceFlowState.pollCount++;
|
||||
|
||||
// Simulate authorization after 3 polls (for testing)
|
||||
if (deviceFlowState.pollCount >= 3) {
|
||||
deviceFlowState.authorized = true;
|
||||
return res.json({
|
||||
access_token: mockAccessToken,
|
||||
token_type: "bearer",
|
||||
scope: "repo,user,workflow",
|
||||
});
|
||||
}
|
||||
|
||||
// Return pending status
|
||||
res.status(400).json({
|
||||
error: "authorization_pending",
|
||||
error_description: "The authorization request is still pending",
|
||||
});
|
||||
}
|
||||
|
||||
// Get authenticated user info
|
||||
export function handleUser(req: Request, res: Response) {
|
||||
console.log("* GitHub User info requested");
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
res.json(mockUser);
|
||||
}
|
||||
|
||||
// Get user emails
|
||||
export function handleUserEmails(req: Request, res: Response) {
|
||||
console.log("* GitHub User emails requested");
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
res.json([
|
||||
{
|
||||
email: "testuser@example.com",
|
||||
primary: true,
|
||||
verified: true,
|
||||
visibility: "public",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// List user repositories
|
||||
export function handleUserRepos(req: Request, res: Response) {
|
||||
console.log("* GitHub User repos requested");
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
// List repos
|
||||
res.json(mockRepos);
|
||||
} else if (req.method === "POST") {
|
||||
// Create repo
|
||||
const { name, private: isPrivate } = req.body;
|
||||
console.log("* Creating repository:", name);
|
||||
|
||||
// Check if repo already exists
|
||||
const existingRepo = mockRepos.find((repo) => repo.name === name);
|
||||
if (existingRepo) {
|
||||
return res.status(422).json({
|
||||
message: "Repository creation failed.",
|
||||
errors: [
|
||||
{
|
||||
resource: "Repository",
|
||||
code: "already_exists",
|
||||
field: "name",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Create new repo
|
||||
const newRepo = {
|
||||
id: mockRepos.length + 1,
|
||||
name,
|
||||
full_name: `${mockUser.login}/${name}`,
|
||||
private: !!isPrivate,
|
||||
owner: { login: mockUser.login },
|
||||
default_branch: "main",
|
||||
};
|
||||
|
||||
res.status(201).json(newRepo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get repository info
|
||||
export function handleRepo(req: Request, res: Response) {
|
||||
console.log("* GitHub Repo info requested");
|
||||
|
||||
const { owner, repo } = req.params;
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`);
|
||||
|
||||
if (!foundRepo) {
|
||||
return res.status(404).json({
|
||||
message: "Not Found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json(foundRepo);
|
||||
}
|
||||
|
||||
// Get repository branches
|
||||
export function handleRepoBranches(req: Request, res: Response) {
|
||||
console.log("* GitHub Repo branches requested");
|
||||
|
||||
const { owner, repo } = req.params;
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
const foundRepo = mockRepos.find((r) => r.full_name === `${owner}/${repo}`);
|
||||
|
||||
if (!foundRepo) {
|
||||
return res.status(404).json({
|
||||
message: "Not Found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json(mockBranches);
|
||||
}
|
||||
|
||||
// Create repository for organization (not implemented in mock)
|
||||
export function handleOrgRepos(req: Request, res: Response) {
|
||||
console.log("* GitHub Org repos requested");
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.includes(mockAccessToken)) {
|
||||
return res.status(401).json({
|
||||
message: "Bad credentials",
|
||||
});
|
||||
}
|
||||
|
||||
// For simplicity, just redirect to user repos for mock
|
||||
handleUserRepos(req, res);
|
||||
}
|
||||
|
||||
// Push event management functions for testing
|
||||
export function handleGetPushEvents(req: Request, res: Response) {
|
||||
console.log("* Getting push events");
|
||||
const { repo } = req.query;
|
||||
|
||||
const events = repo ? pushEvents.filter((e) => e.repo === repo) : pushEvents;
|
||||
|
||||
res.json(events);
|
||||
}
|
||||
|
||||
export function handleClearPushEvents(req: Request, res: Response) {
|
||||
console.log("* Clearing push events");
|
||||
pushEvents.length = 0;
|
||||
res.json({ cleared: true, timestamp: new Date() });
|
||||
}
|
||||
|
||||
// Handle Git operations (push, pull, clone, etc.) using git-http-mock-server
|
||||
export function handleGitPush(req: Request, res: Response, next?: Function) {
|
||||
console.log("* GitHub Git operation requested:", req.method, req.url);
|
||||
|
||||
// Log request headers to see git operation details
|
||||
console.log("* Git Headers:", {
|
||||
"git-protocol": req.headers["git-protocol"],
|
||||
"content-type": req.headers["content-type"],
|
||||
"user-agent": req.headers["user-agent"],
|
||||
});
|
||||
|
||||
// Create a unique temporary directory for this request
|
||||
const mockReposRoot = fs.mkdtempSync(
|
||||
path.join(
|
||||
os.tmpdir(),
|
||||
"dyad-git-mock-" + Math.random().toString(36).substring(2, 15),
|
||||
),
|
||||
);
|
||||
console.error(`* Created temporary git repos directory: ${mockReposRoot}`);
|
||||
|
||||
// Create git middleware instance for this request
|
||||
const gitHttpMiddleware = gitHttpMiddlewareFactory({
|
||||
root: mockReposRoot,
|
||||
route: "/github/git",
|
||||
glob: "*.git",
|
||||
});
|
||||
|
||||
// Extract repo name from URL path like /github/git/testuser/test-repo.git
|
||||
// The middleware expects the repo name as the basename after the route
|
||||
const urlPath = req.url;
|
||||
const match = urlPath.match(/\/github\/git\/[^/]+\/([^/.]+)\.git/);
|
||||
const repoName = match?.[1];
|
||||
|
||||
if (repoName) {
|
||||
console.log(`* Git operation for repo: ${repoName}`);
|
||||
|
||||
// Track push events if this is a git-receive-pack (push) operation
|
||||
if (req.url.includes("/git-receive-pack") && req.method === "POST") {
|
||||
console.log("* Git PUSH operation detected for repo:", repoName);
|
||||
|
||||
// Collect request body to parse git protocol
|
||||
let body = "";
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on("end", () => {
|
||||
try {
|
||||
// Parse git pack protocol for branch refs
|
||||
// Git protocol sends refs in format: "old-sha new-sha refs/heads/branch-name"
|
||||
const lines = body.split("\n");
|
||||
lines.forEach((line) => {
|
||||
// Look for lines containing refs/heads/
|
||||
const refMatch = line.match(
|
||||
/([0-9a-f]{40})\s+([0-9a-f]{40})\s+refs\/heads\/([^\s\u0000]+)/,
|
||||
);
|
||||
if (refMatch) {
|
||||
const [, oldSha, newSha, branchName] = refMatch;
|
||||
const isDelete = newSha === "0".repeat(40);
|
||||
const isCreate = oldSha === "0".repeat(40);
|
||||
|
||||
let operation: "push" | "create" | "delete" = "push";
|
||||
if (isDelete) operation = "delete";
|
||||
else if (isCreate) operation = "create";
|
||||
|
||||
pushEvents.push({
|
||||
timestamp: new Date(),
|
||||
repo: repoName,
|
||||
branch: branchName,
|
||||
operation,
|
||||
commitSha: isDelete ? oldSha : newSha,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`* Recorded ${operation} to ${repoName}/${branchName}, commit: ${isDelete ? oldSha : newSha}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("* Error parsing git protocol:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the bare git repository exists for this repo
|
||||
const bareRepoPath = path.join(mockReposRoot, `${repoName}.git`);
|
||||
console.log(`* Creating bare git repository at: ${bareRepoPath}`);
|
||||
try {
|
||||
fs.mkdirSync(bareRepoPath, { recursive: true });
|
||||
// Initialize as bare repository
|
||||
const { execSync } = require("child_process");
|
||||
execSync(`git init --bare`, { cwd: bareRepoPath });
|
||||
console.log(
|
||||
`* Successfully created bare git repository: ${repoName}.git`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`* Failed to create bare git repository:`, error);
|
||||
return res.status(500).json({
|
||||
message: "Failed to initialize git repository",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
// Rewrite the URL to match what the middleware expects
|
||||
// Change /github/git/testuser/test-repo.git/... to /github/git/test-repo.git/...
|
||||
const rewrittenUrl = req.url.replace(
|
||||
/\/github\/git\/[^/]+\//,
|
||||
"/github/git/",
|
||||
);
|
||||
req.url = rewrittenUrl;
|
||||
console.log(`* Rewritten URL from ${urlPath} to ${rewrittenUrl}`);
|
||||
}
|
||||
|
||||
// Use git-http-mock-server middleware to handle the actual git operations
|
||||
gitHttpMiddleware(
|
||||
req,
|
||||
res,
|
||||
next ||
|
||||
(() => {
|
||||
// Fallback if middleware doesn't handle the request
|
||||
console.log(
|
||||
`* Git middleware did not handle request: ${req.method} ${req.url}`,
|
||||
);
|
||||
res.status(404).json({
|
||||
message: "Git operation not supported",
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user