mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 20:13:21 +00:00
feat: Clone, commit and push as bot, not user (#174)
This commit is contained in:
parent
fda6529419
commit
1d0a1cce6f
5 changed files with 34 additions and 89 deletions
|
|
@ -50,6 +50,8 @@ DAYTONA_API_KEY=""
|
||||||
# Should be the same value as the one used in the web app.
|
# Should be the same value as the one used in the web app.
|
||||||
# Can be generated via: `openssl rand -hex 32`
|
# Can be generated via: `openssl rand -hex 32`
|
||||||
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
||||||
|
# Used for setting the git user name & email for commits.
|
||||||
|
GITHUB_APP_NAME="open-swe-dev"
|
||||||
```
|
```
|
||||||
|
|
||||||
And the web `.env` file should contain the following variables:
|
And the web `.env` file should contain the following variables:
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,5 @@ DAYTONA_API_KEY=""
|
||||||
# Encryption key for GitHub tokens (32-byte hex string for AES-256)
|
# Encryption key for GitHub tokens (32-byte hex string for AES-256)
|
||||||
# Should be the same value as the one used in the web app.
|
# Should be the same value as the one used in the web app.
|
||||||
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
GITHUB_TOKEN_ENCRYPTION_KEY=""
|
||||||
|
# Used for setting the git user name & email for commits.
|
||||||
|
GITHUB_APP_NAME="open-swe-dev"
|
||||||
|
|
@ -29,7 +29,7 @@ export async function initialize(
|
||||||
state: GraphState,
|
state: GraphState,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
): Promise<GraphUpdate> {
|
): Promise<GraphUpdate> {
|
||||||
const { githubAccessToken } = getGitHubTokensFromConfig(config);
|
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
||||||
const { sandboxSessionId, targetRepository } = state;
|
const { sandboxSessionId, targetRepository } = state;
|
||||||
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
|
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ export async function initialize(
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await cloneRepo(sandbox, targetRepository, {
|
const res = await cloneRepo(sandbox, targetRepository, {
|
||||||
githubAccessToken,
|
githubInstallationToken,
|
||||||
stateBranchName: state.branchName,
|
stateBranchName: state.branchName,
|
||||||
});
|
});
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
|
|
@ -70,7 +70,7 @@ export async function initialize(
|
||||||
|
|
||||||
logger.info(`Configuring git user for repository at "${absoluteRepoDir}"...`);
|
logger.info(`Configuring git user for repository at "${absoluteRepoDir}"...`);
|
||||||
await configureGitUserInRepo(absoluteRepoDir, sandbox, {
|
await configureGitUserInRepo(absoluteRepoDir, sandbox, {
|
||||||
githubAccessToken,
|
githubInstallationToken,
|
||||||
owner: targetRepository.owner,
|
owner: targetRepository.owner,
|
||||||
repo: targetRepository.repo,
|
repo: targetRepository.repo,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export async function openPullRequest(
|
||||||
"Failed to open pull request: No sandbox session ID found in state.",
|
"Failed to open pull request: No sandbox session ID found in state.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { githubAccessToken } = getGitHubTokensFromConfig(config);
|
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
||||||
|
|
||||||
const sandbox = await daytonaClient().get(sandboxSessionId);
|
const sandbox = await daytonaClient().get(sandboxSessionId);
|
||||||
|
|
||||||
|
|
@ -133,7 +133,7 @@ export async function openPullRequest(
|
||||||
headBranch: branchName ?? getBranchName(config),
|
headBranch: branchName ?? getBranchName(config),
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
githubAccessToken,
|
githubInstallationToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
let sandboxDeleted = false;
|
let sandboxDeleted = false;
|
||||||
|
|
|
||||||
|
|
@ -135,81 +135,16 @@ export async function checkoutBranch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitHubUserResponse {
|
|
||||||
login: string;
|
|
||||||
id: number;
|
|
||||||
name: string | null;
|
|
||||||
email: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getGitUserDetailsFromGitHub(githubToken: string): Promise<{
|
|
||||||
userName?: string;
|
|
||||||
userEmail?: string;
|
|
||||||
}> {
|
|
||||||
try {
|
|
||||||
// Try with Bearer token first (for GitHub App installation tokens)
|
|
||||||
const response = await fetch("https://api.github.com/user", {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${githubToken}`,
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
"User-Agent": "OpenSWE",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logger.error(`Failed to fetch GitHub user info`, {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
});
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = (await response.json()) as GitHubUserResponse;
|
|
||||||
const fetchedUserName = userData.name || userData.login;
|
|
||||||
let fetchedUserEmail = userData.email; // This can be string | null
|
|
||||||
|
|
||||||
if (!fetchedUserEmail && userData.id && userData.login) {
|
|
||||||
fetchedUserEmail = `${userData.id}+${userData.login}@users.noreply.github.com`;
|
|
||||||
} else if (!fetchedUserEmail && userData.login) {
|
|
||||||
fetchedUserEmail = `${userData.login}@users.noreply.github.com`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalUserName = fetchedUserName || undefined;
|
|
||||||
const finalUserEmail = fetchedUserEmail || undefined;
|
|
||||||
|
|
||||||
if (!finalUserName) {
|
|
||||||
logger.warn("Could not determine GitHub username from API response.");
|
|
||||||
}
|
|
||||||
if (!finalUserEmail) {
|
|
||||||
logger.warn("Could not determine GitHub user email from API response.");
|
|
||||||
}
|
|
||||||
logger.info("Successfully fetched GitHub user info", {
|
|
||||||
userName: finalUserName,
|
|
||||||
userEmail: finalUserEmail,
|
|
||||||
});
|
|
||||||
return { userName: finalUserName, userEmail: finalUserEmail };
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`Error fetching GitHub user info`, {
|
|
||||||
...(e instanceof Error && {
|
|
||||||
name: e.name,
|
|
||||||
message: e.message,
|
|
||||||
stack: e.stack,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function configureGitUserInRepo(
|
export async function configureGitUserInRepo(
|
||||||
absoluteRepoDir: string,
|
absoluteRepoDir: string,
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
args: {
|
args: {
|
||||||
githubAccessToken: string;
|
githubInstallationToken: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { githubAccessToken, owner, repo } = args;
|
const { githubInstallationToken, owner, repo } = args;
|
||||||
let needsGitConfig = false;
|
let needsGitConfig = false;
|
||||||
try {
|
try {
|
||||||
const nameCheck = await sandbox.process.executeCommand(
|
const nameCheck = await sandbox.process.executeCommand(
|
||||||
|
|
@ -251,7 +186,7 @@ export async function configureGitUserInRepo(
|
||||||
try {
|
try {
|
||||||
// Set the remote URL with the token using the provided owner and repo
|
// Set the remote URL with the token using the provided owner and repo
|
||||||
const setRemoteOutput = await sandbox.process.executeCommand(
|
const setRemoteOutput = await sandbox.process.executeCommand(
|
||||||
`git remote set-url origin https://x-access-token:${githubAccessToken}@github.com/${owner}/${repo}.git`,
|
`git remote set-url origin https://x-access-token:${githubInstallationToken}@github.com/${owner}/${repo}.git`,
|
||||||
absoluteRepoDir,
|
absoluteRepoDir,
|
||||||
undefined,
|
undefined,
|
||||||
TIMEOUT_SEC,
|
TIMEOUT_SEC,
|
||||||
|
|
@ -275,13 +210,16 @@ export async function configureGitUserInRepo(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsGitConfig) {
|
if (needsGitConfig) {
|
||||||
const { userName, userEmail } =
|
const botAppName = process.env.GITHUB_APP_NAME;
|
||||||
await getGitUserDetailsFromGitHub(githubAccessToken);
|
if (!botAppName) {
|
||||||
|
logger.error("GITHUB_APP_NAME environment variable is not set.");
|
||||||
|
throw new Error("GITHUB_APP_NAME environment variable is not set.");
|
||||||
|
}
|
||||||
|
const userName = `${botAppName}[bot]`;
|
||||||
|
const userEmail = `${botAppName}@users.noreply.github.com`;
|
||||||
|
|
||||||
// Set user name - use fetched name or fallback to "GitHub App User"
|
|
||||||
const nameToUse = userName || "GitHub App User";
|
|
||||||
const configUserNameOutput = await sandbox.process.executeCommand(
|
const configUserNameOutput = await sandbox.process.executeCommand(
|
||||||
`git config user.name "${nameToUse}"`,
|
`git config user.name "${userName}"`,
|
||||||
absoluteRepoDir,
|
absoluteRepoDir,
|
||||||
undefined,
|
undefined,
|
||||||
TIMEOUT_SEC,
|
TIMEOUT_SEC,
|
||||||
|
|
@ -291,13 +229,11 @@ export async function configureGitUserInRepo(
|
||||||
configUserNameOutput,
|
configUserNameOutput,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.info(`Set git user.name to '${nameToUse}' successfully.`);
|
logger.info(`Set git user.name to '${userName}' successfully.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set user email - use fetched email or fallback to a generic noreply address
|
|
||||||
const emailToUse = userEmail || `${repo}-bot@noreply.github.com`;
|
|
||||||
const configUserEmailOutput = await sandbox.process.executeCommand(
|
const configUserEmailOutput = await sandbox.process.executeCommand(
|
||||||
`git config user.email "${emailToUse}"`,
|
`git config user.email "${userEmail}"`,
|
||||||
absoluteRepoDir,
|
absoluteRepoDir,
|
||||||
undefined,
|
undefined,
|
||||||
TIMEOUT_SEC,
|
TIMEOUT_SEC,
|
||||||
|
|
@ -307,7 +243,7 @@ export async function configureGitUserInRepo(
|
||||||
configUserEmailOutput,
|
configUserEmailOutput,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.info(`Set git user.email to '${emailToUse}' successfully.`);
|
logger.info(`Set git user.email to '${userEmail}' successfully.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -482,17 +418,17 @@ export async function createPullRequest({
|
||||||
headBranch,
|
headBranch,
|
||||||
title,
|
title,
|
||||||
body = "",
|
body = "",
|
||||||
githubAccessToken,
|
githubInstallationToken,
|
||||||
}: {
|
}: {
|
||||||
owner: string;
|
owner: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
headBranch: string;
|
headBranch: string;
|
||||||
title: string;
|
title: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
githubAccessToken: string;
|
githubInstallationToken: string;
|
||||||
}) {
|
}) {
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: githubAccessToken,
|
auth: githubInstallationToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -545,7 +481,12 @@ export async function createPullRequest({
|
||||||
logger.info(
|
logger.info(
|
||||||
"Pull request already exists. Getting existing pull request...",
|
"Pull request already exists. Getting existing pull request...",
|
||||||
);
|
);
|
||||||
return getExistingPullRequest(owner, repo, headBranch, githubAccessToken);
|
return getExistingPullRequest(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
headBranch,
|
||||||
|
githubInstallationToken,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(`Failed to create pull request`, {
|
logger.error(`Failed to create pull request`, {
|
||||||
|
|
@ -585,7 +526,7 @@ export async function cloneRepo(
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
targetRepository: TargetRepository,
|
targetRepository: TargetRepository,
|
||||||
args: {
|
args: {
|
||||||
githubAccessToken: string;
|
githubInstallationToken: string;
|
||||||
stateBranchName?: string;
|
stateBranchName?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|
@ -593,7 +534,7 @@ export async function cloneRepo(
|
||||||
const gitCloneCommand = ["git", "clone"];
|
const gitCloneCommand = ["git", "clone"];
|
||||||
|
|
||||||
// Use x-access-token format for better GitHub authentication
|
// Use x-access-token format for better GitHub authentication
|
||||||
const repoUrlWithToken = `https://x-access-token:${args.githubAccessToken}@github.com/${targetRepository.owner}/${targetRepository.repo}.git`;
|
const repoUrlWithToken = `https://x-access-token:${args.githubInstallationToken}@github.com/${targetRepository.owner}/${targetRepository.repo}.git`;
|
||||||
|
|
||||||
const branchName = args.stateBranchName || targetRepository.branch;
|
const branchName = args.stateBranchName || targetRepository.branch;
|
||||||
if (branchName) {
|
if (branchName) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue