Decouple git from github errors (#1347)
* Decouple git from github errors
* Fix git error display (vibe-kanban 7352dadc)
After the last few commits git cli not logged in error does not get displayed to the user. Network tab shows this:
{
"success": false,
"data": null,
"error_data": {
"type": "git_cli_not_logged_in"
},
"message": null
}
This commit is contained in:
@@ -93,7 +93,6 @@ fn generate_types_content() -> String {
|
||||
server::routes::tasks::CreateAndStartTaskRequest::decl(),
|
||||
server::routes::task_attempts::CreateGitHubPrRequest::decl(),
|
||||
server::routes::images::ImageResponse::decl(),
|
||||
services::services::github::GitHubServiceError::decl(),
|
||||
services::services::config::Config::decl(),
|
||||
services::services::config::NotificationConfig::decl(),
|
||||
services::services::config::ThemeMode::decl(),
|
||||
@@ -138,6 +137,7 @@ fn generate_types_content() -> String {
|
||||
server::routes::task_attempts::gh_cli_setup::GhCliSetupError::decl(),
|
||||
server::routes::task_attempts::RebaseTaskAttemptRequest::decl(),
|
||||
server::routes::task_attempts::GitOperationError::decl(),
|
||||
server::routes::task_attempts::CreatePrError::decl(),
|
||||
server::routes::task_attempts::CommitInfo::decl(),
|
||||
server::routes::task_attempts::BranchStatus::decl(),
|
||||
services::services::git::ConflictOp::decl(),
|
||||
|
||||
@@ -36,7 +36,7 @@ use git2::BranchType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use services::services::{
|
||||
container::ContainerService,
|
||||
git::{ConflictOp, WorktreeResetOptions},
|
||||
git::{ConflictOp, GitCliError, GitServiceError, WorktreeResetOptions},
|
||||
github::{CreatePrRequest, GitHubService, GitHubServiceError},
|
||||
};
|
||||
use sqlx::Error as SqlxError;
|
||||
@@ -605,11 +605,21 @@ pub async fn push_task_attempt_branch(
|
||||
Ok(ResponseJson(ApiResponse::success(())))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type", rename_all = "snake_case")]
|
||||
pub enum CreatePrError {
|
||||
GithubCliNotInstalled,
|
||||
GithubCliNotLoggedIn,
|
||||
GitCliNotLoggedIn,
|
||||
GitCliNotInstalled,
|
||||
}
|
||||
|
||||
pub async fn create_github_pr(
|
||||
Extension(task_attempt): Extension<TaskAttempt>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Json(request): Json<CreateGitHubPrRequest>,
|
||||
) -> Result<ResponseJson<ApiResponse<String, GitHubServiceError>>, ApiError> {
|
||||
) -> Result<ResponseJson<ApiResponse<String, CreatePrError>>, ApiError> {
|
||||
let github_config = deployment.config().read().await.github.clone();
|
||||
// Get the task attempt to access the stored target branch
|
||||
let target_branch = request.target_branch.unwrap_or_else(|| {
|
||||
@@ -642,14 +652,19 @@ pub async fn create_github_pr(
|
||||
.push_to_github(&workspace_path, &task_attempt.branch)
|
||||
{
|
||||
tracing::error!("Failed to push branch to GitHub: {}", e);
|
||||
let gh_e = GitHubServiceError::from(e);
|
||||
if gh_e.is_api_data() {
|
||||
return Ok(ResponseJson(ApiResponse::error_with_data(gh_e)));
|
||||
} else {
|
||||
return Ok(ResponseJson(ApiResponse::error(
|
||||
format!("Failed to push branch to GitHub: {}", gh_e).as_str(),
|
||||
match e {
|
||||
GitServiceError::GitCLI(GitCliError::AuthFailed(_)) => {
|
||||
return Ok(ResponseJson(ApiResponse::error_with_data(
|
||||
CreatePrError::GitCliNotLoggedIn,
|
||||
)));
|
||||
}
|
||||
GitServiceError::GitCLI(GitCliError::NotAvailable) => {
|
||||
return Ok(ResponseJson(ApiResponse::error_with_data(
|
||||
CreatePrError::GitCliNotInstalled,
|
||||
)));
|
||||
}
|
||||
_ => return Err(ApiError::GitService(e)),
|
||||
}
|
||||
}
|
||||
|
||||
let norm_target_branch_name = if matches!(
|
||||
@@ -723,12 +738,14 @@ pub async fn create_github_pr(
|
||||
task_attempt.id,
|
||||
e
|
||||
);
|
||||
if e.is_api_data() {
|
||||
Ok(ResponseJson(ApiResponse::error_with_data(e)))
|
||||
} else {
|
||||
Ok(ResponseJson(ApiResponse::error(
|
||||
format!("Failed to create PR: {}", e).as_str(),
|
||||
)))
|
||||
match &e {
|
||||
GitHubServiceError::GhCliNotInstalled(_) => Ok(ResponseJson(
|
||||
ApiResponse::error_with_data(CreatePrError::GithubCliNotInstalled),
|
||||
)),
|
||||
GitHubServiceError::AuthFailed(_) => Ok(ResponseJson(
|
||||
ApiResponse::error_with_data(CreatePrError::GithubCliNotLoggedIn),
|
||||
)),
|
||||
_ => Err(ApiError::GitHubService(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,96 +3,61 @@ use std::time::Duration;
|
||||
use backon::{ExponentialBuilder, Retryable};
|
||||
use db::models::merge::PullRequestInfo;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tokio::task;
|
||||
use tracing::info;
|
||||
use ts_rs::TS;
|
||||
|
||||
mod cli;
|
||||
|
||||
use cli::{GhCli, GhCliError};
|
||||
|
||||
use crate::services::git::{GitCliError, GitServiceError};
|
||||
|
||||
#[derive(Debug, Error, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[ts(use_ts_enum)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GitHubServiceError {
|
||||
#[ts(skip)]
|
||||
#[error("Repository error: {0}")]
|
||||
Repository(String),
|
||||
#[ts(skip)]
|
||||
#[error("Pull request error: {0}")]
|
||||
PullRequest(String),
|
||||
#[error("GitHub token is invalid or expired.")]
|
||||
TokenInvalid,
|
||||
#[error("Insufficient permissions")]
|
||||
InsufficientPermissions,
|
||||
#[error("GitHub repository not found or no access")]
|
||||
RepoNotFoundOrNoAccess,
|
||||
#[error("GitHub authentication failed: {0}")]
|
||||
AuthFailed(GhCliError),
|
||||
#[error("Insufficient permissions: {0}")]
|
||||
InsufficientPermissions(GhCliError),
|
||||
#[error("GitHub repository not found or no access: {0}")]
|
||||
RepoNotFoundOrNoAccess(GhCliError),
|
||||
#[error(
|
||||
"GitHub CLI is not installed or not available in PATH. Please install it from https://cli.github.com/ and authenticate with 'gh auth login'"
|
||||
)]
|
||||
GhCliNotInstalled,
|
||||
#[ts(skip)]
|
||||
#[serde(skip)]
|
||||
#[error(transparent)]
|
||||
GitService(GitServiceError),
|
||||
GhCliNotInstalled(GhCliError),
|
||||
}
|
||||
|
||||
impl From<GhCliError> for GitHubServiceError {
|
||||
fn from(error: GhCliError) -> Self {
|
||||
match error {
|
||||
GhCliError::AuthFailed(_) => Self::TokenInvalid,
|
||||
GhCliError::NotAvailable => Self::GhCliNotInstalled,
|
||||
match &error {
|
||||
GhCliError::AuthFailed(_) => Self::AuthFailed(error),
|
||||
GhCliError::NotAvailable => Self::GhCliNotInstalled(error),
|
||||
GhCliError::CommandFailed(msg) => {
|
||||
let lower = msg.to_ascii_lowercase();
|
||||
if lower.contains("403") || lower.contains("forbidden") {
|
||||
Self::InsufficientPermissions
|
||||
Self::InsufficientPermissions(error)
|
||||
} else if lower.contains("404") || lower.contains("not found") {
|
||||
Self::RepoNotFoundOrNoAccess
|
||||
Self::RepoNotFoundOrNoAccess(error)
|
||||
} else {
|
||||
Self::PullRequest(msg)
|
||||
Self::PullRequest(msg.to_string())
|
||||
}
|
||||
}
|
||||
GhCliError::UnexpectedOutput(msg) => Self::PullRequest(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GitServiceError> for GitHubServiceError {
|
||||
fn from(error: GitServiceError) -> Self {
|
||||
match error {
|
||||
GitServiceError::GitCLI(GitCliError::AuthFailed(_)) => Self::TokenInvalid,
|
||||
GitServiceError::GitCLI(GitCliError::CommandFailed(msg)) => {
|
||||
let lower = msg.to_ascii_lowercase();
|
||||
if lower.contains("the requested url returned error: 403") {
|
||||
Self::InsufficientPermissions
|
||||
} else if lower.contains("the requested url returned error: 404") {
|
||||
Self::RepoNotFoundOrNoAccess
|
||||
} else {
|
||||
Self::GitService(GitServiceError::GitCLI(GitCliError::CommandFailed(msg)))
|
||||
}
|
||||
}
|
||||
other => Self::GitService(other),
|
||||
GhCliError::UnexpectedOutput(msg) => Self::PullRequest(msg.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GitHubServiceError {
|
||||
pub fn is_api_data(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
GitHubServiceError::TokenInvalid
|
||||
| GitHubServiceError::InsufficientPermissions
|
||||
| GitHubServiceError::RepoNotFoundOrNoAccess
|
||||
| GitHubServiceError::GhCliNotInstalled
|
||||
)
|
||||
}
|
||||
|
||||
pub fn should_retry(&self) -> bool {
|
||||
!self.is_api_data()
|
||||
!matches!(
|
||||
self,
|
||||
GitHubServiceError::AuthFailed(_)
|
||||
| GitHubServiceError::InsufficientPermissions(_)
|
||||
| GitHubServiceError::RepoNotFoundOrNoAccess(_)
|
||||
| GitHubServiceError::GhCliNotInstalled(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,8 +133,8 @@ impl GitHubService {
|
||||
))
|
||||
})?
|
||||
.map_err(|err| match err {
|
||||
GhCliError::NotAvailable => GitHubServiceError::GhCliNotInstalled,
|
||||
GhCliError::AuthFailed(_) => GitHubServiceError::TokenInvalid,
|
||||
GhCliError::NotAvailable => GitHubServiceError::GhCliNotInstalled(err),
|
||||
GhCliError::AuthFailed(_) => GitHubServiceError::AuthFailed(err),
|
||||
GhCliError::CommandFailed(msg) => {
|
||||
GitHubServiceError::Repository(format!("GitHub CLI auth check failed: {msg}"))
|
||||
}
|
||||
|
||||
@@ -16,12 +16,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { attemptsApi } from '@/lib/api.ts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
GitBranch,
|
||||
GitHubServiceError,
|
||||
TaskAttempt,
|
||||
TaskWithAttemptStatus,
|
||||
} from 'shared/types';
|
||||
import { GitBranch, TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
|
||||
import { projectsApi } from '@/lib/api.ts';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
||||
@@ -166,8 +161,10 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
|
||||
};
|
||||
|
||||
if (result.error) {
|
||||
switch (result.error) {
|
||||
case GitHubServiceError.GH_CLI_NOT_INSTALLED: {
|
||||
if (
|
||||
result.error.type === 'github_cli_not_installed' ||
|
||||
result.error.type === 'github_cli_not_logged_in'
|
||||
) {
|
||||
if (isMacEnvironment) {
|
||||
await showGhCliSetupDialog();
|
||||
} else {
|
||||
@@ -180,33 +177,16 @@ const CreatePRDialogImpl = NiceModal.create<CreatePRDialogProps>(
|
||||
setError(ui.variant ? null : ui.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case GitHubServiceError.TOKEN_INVALID: {
|
||||
if (isMacEnvironment) {
|
||||
await showGhCliSetupDialog();
|
||||
} else {
|
||||
const ui = mapGhCliErrorToUi(
|
||||
'SETUP_HELPER_NOT_SUPPORTED',
|
||||
defaultGhCliErrorMessage,
|
||||
t
|
||||
);
|
||||
setGhCliHelp(ui.variant ? ui : null);
|
||||
setError(ui.variant ? null : ui.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case GitHubServiceError.INSUFFICIENT_PERMISSIONS:
|
||||
setError(t('createPrDialog.errors.insufficientPermissions'));
|
||||
setGhCliHelp(null);
|
||||
return;
|
||||
case GitHubServiceError.REPO_NOT_FOUND_OR_NO_ACCESS:
|
||||
setError(t('createPrDialog.errors.repoNotFoundOrNoAccess'));
|
||||
setGhCliHelp(null);
|
||||
return;
|
||||
default:
|
||||
setError(
|
||||
result.message || t('createPrDialog.errors.failedToCreate')
|
||||
);
|
||||
} else if (
|
||||
result.error.type === 'git_cli_not_installed' ||
|
||||
result.error.type === 'git_cli_not_logged_in'
|
||||
) {
|
||||
const gitCliErrorKey =
|
||||
result.error.type === 'git_cli_not_logged_in'
|
||||
? 'createPrDialog.errors.gitCliNotLoggedIn'
|
||||
: 'createPrDialog.errors.gitCliNotInstalled';
|
||||
|
||||
setError(result.message || t(gitCliErrorKey));
|
||||
setGhCliHelp(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -341,7 +341,9 @@
|
||||
"errors": {
|
||||
"insufficientPermissions": "Insufficient permissions. Please ensure the GitHub CLI has the necessary permissions.",
|
||||
"repoNotFoundOrNoAccess": "Repository not found or no access. Please check your repository access and ensure you are authenticated.",
|
||||
"failedToCreate": "Failed to create GitHub PR"
|
||||
"failedToCreate": "Failed to create GitHub PR",
|
||||
"gitCliNotLoggedIn": "Git is not authenticated. Run \"gh auth login\" (or configure Git credentials) and try again.",
|
||||
"gitCliNotInstalled": "Git CLI is not installed. Install Git to create a PR."
|
||||
},
|
||||
"loginRequired": {
|
||||
"title": "Sign in to create a pull request",
|
||||
|
||||
@@ -117,7 +117,9 @@
|
||||
"errors": {
|
||||
"insufficientPermissions": "Permisos insuficientes. Por favor asegúrate de que la CLI de GitHub tenga los permisos necesarios.",
|
||||
"repoNotFoundOrNoAccess": "Repositorio no encontrado o sin acceso. Por favor verifica el acceso al repositorio y asegúrate de estar autenticado.",
|
||||
"failedToCreate": "Error al crear PR de GitHub"
|
||||
"failedToCreate": "Error al crear PR de GitHub",
|
||||
"gitCliNotLoggedIn": "Git no está autenticado. Ejecuta \"gh auth login\" (o configura las credenciales de Git) e inténtalo de nuevo.",
|
||||
"gitCliNotInstalled": "Git CLI no está instalado. Instala Git para crear una PR."
|
||||
},
|
||||
"loginRequired": {
|
||||
"title": "Inicia sesión para crear un pull request",
|
||||
|
||||
@@ -117,7 +117,9 @@
|
||||
"errors": {
|
||||
"insufficientPermissions": "権限が不足しています。GitHub CLIに必要な権限があることを確認してください。",
|
||||
"repoNotFoundOrNoAccess": "リポジトリが見つからないか、アクセス権がありません。リポジトリへのアクセス権を確認し、認証されていることを確認してください。",
|
||||
"failedToCreate": "GitHub PRの作成に失敗しました"
|
||||
"failedToCreate": "GitHub PRの作成に失敗しました",
|
||||
"gitCliNotLoggedIn": "Gitが認証されていません。\"gh auth login\" を実行するかGitの認証情報を設定してから再試行してください。",
|
||||
"gitCliNotInstalled": "Git CLIがインストールされていません。PRを作成するにはGitをインストールしてください。"
|
||||
},
|
||||
"loginRequired": {
|
||||
"title": "プルリクエストを作成するにはサインインしてください",
|
||||
|
||||
@@ -117,7 +117,9 @@
|
||||
"errors": {
|
||||
"insufficientPermissions": "권한이 부족합니다. GitHub CLI에 필요한 권한이 있는지 확인하세요.",
|
||||
"repoNotFoundOrNoAccess": "저장소를 찾을 수 없거나 액세스 권한이 없습니다. 저장소 액세스를 확인하고 인증되었는지 확인하세요.",
|
||||
"failedToCreate": "GitHub PR 생성에 실패했습니다"
|
||||
"failedToCreate": "GitHub PR 생성에 실패했습니다",
|
||||
"gitCliNotLoggedIn": "Git이 인증되지 않았습니다. \"gh auth login\"을 실행하거나 Git 자격 증명을 설정한 후 다시 시도하세요.",
|
||||
"gitCliNotInstalled": "Git CLI가 설치되어 있지 않습니다. PR을 생성하려면 Git을 설치하세요."
|
||||
},
|
||||
"loginRequired": {
|
||||
"title": "Pull Request를 만들려면 로그인하세요",
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
UpdateTask,
|
||||
UpdateTag,
|
||||
UserSystemInfo,
|
||||
GitHubServiceError,
|
||||
UpdateRetryFollowUpDraftRequest,
|
||||
McpServerQuery,
|
||||
UpdateMcpServersBody,
|
||||
@@ -71,6 +70,7 @@ import {
|
||||
CommitCompareResult,
|
||||
OpenEditorResponse,
|
||||
OpenEditorRequest,
|
||||
CreatePrError,
|
||||
} from 'shared/types';
|
||||
|
||||
// Re-export types for convenience
|
||||
@@ -615,12 +615,12 @@ export const attemptsApi = {
|
||||
createPR: async (
|
||||
attemptId: string,
|
||||
data: CreateGitHubPrRequest
|
||||
): Promise<Result<string, GitHubServiceError>> => {
|
||||
): Promise<Result<string, CreatePrError>> => {
|
||||
const response = await makeRequest(`/api/task-attempts/${attemptId}/pr`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return handleApiResponseAsResult<string, GitHubServiceError>(response);
|
||||
return handleApiResponseAsResult<string, CreatePrError>(response);
|
||||
},
|
||||
|
||||
startDevServer: async (attemptId: string): Promise<void> => {
|
||||
|
||||
@@ -174,8 +174,6 @@ export type CreateGitHubPrRequest = { title: string, body: string | null, target
|
||||
|
||||
export type ImageResponse = { id: string, file_path: string, original_name: string, mime_type: string | null, size_bytes: bigint, hash: string, created_at: string, updated_at: string, };
|
||||
|
||||
export enum GitHubServiceError { TOKEN_INVALID = "TOKEN_INVALID", INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS", REPO_NOT_FOUND_OR_NO_ACCESS = "REPO_NOT_FOUND_OR_NO_ACCESS", GH_CLI_NOT_INSTALLED = "GH_CLI_NOT_INSTALLED" }
|
||||
|
||||
export type Config = { config_version: string, theme: ThemeMode, executor_profile: ExecutorProfileId, disclaimer_acknowledged: boolean, onboarding_acknowledged: boolean, notifications: NotificationConfig, editor: EditorConfig, github: GitHubConfig, analytics_enabled: boolean, workspace_dir: string | null, last_app_version: string | null, show_release_notes: boolean, language: UiLanguage, git_branch_prefix: string, showcases: ShowcaseState, };
|
||||
|
||||
export type NotificationConfig = { sound_enabled: boolean, push_enabled: boolean, sound_file: SoundFile, };
|
||||
@@ -300,6 +298,8 @@ export type RebaseTaskAttemptRequest = { old_base_branch: string | null, new_bas
|
||||
|
||||
export type GitOperationError = { "type": "merge_conflicts", message: string, op: ConflictOp, } | { "type": "rebase_in_progress" };
|
||||
|
||||
export type CreatePrError = { "type": "github_cli_not_installed" } | { "type": "github_cli_not_logged_in" } | { "type": "git_cli_not_logged_in" } | { "type": "git_cli_not_installed" };
|
||||
|
||||
export type CommitInfo = { sha: string, subject: string, };
|
||||
|
||||
export type BranchStatus = { commits_behind: number | null, commits_ahead: number | null, has_uncommitted_changes: boolean | null, head_oid: string | null, uncommitted_count: number | null, untracked_count: number | null, target_branch_name: string, remote_commits_behind: number | null, remote_commits_ahead: number | null, merges: Array<Merge>,
|
||||
|
||||
Reference in New Issue
Block a user