Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 35 additions & 24 deletions backend/airweave/platform/configs/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Configuration classes for platform components."""

from typing import Optional
from typing import Any, Optional

from pydantic import Field, field_validator

Expand Down Expand Up @@ -58,14 +58,13 @@ class BitbucketConfig(SourceConfig):

@field_validator("file_extensions", mode="before")
@classmethod
def parse_file_extensions(cls, value):
def parse_file_extensions(cls, value: Any) -> list[str]:
"""Convert string input to list if needed."""
if isinstance(value, str):
if not value.strip():
return []
# Split by commas and strip whitespace
return [ext.strip() for ext in value.split(",") if ext.strip()]
return value
return value # type: ignore[no-any-return]


class BoxConfig(SourceConfig):
Expand Down Expand Up @@ -150,9 +149,11 @@ class GitHubConfig(SourceConfig):

repo_name: str = Field(
title="Repository Name",
description="Repository to sync in owner/repo format (e.g., 'airweave-ai/airweave')",
description=(
"Repository to sync in owner/repo format (e.g., 'airweave-ai/airweave') "
"or full GitHub URL (e.g., 'https://github.com/airweave-ai/airweave')"
),
min_length=3,
pattern=r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+$",
)
branch: str = Field(
default="",
Expand All @@ -171,6 +172,18 @@ class GitHubConfig(SourceConfig):
),
)

@field_validator("repo_name", mode="before")
@classmethod
def normalize_repo_name(cls, v: str) -> str:
"""Strip GitHub URL prefix if provided, normalizing to owner/repo format."""
if isinstance(v, str):
v = v.strip().rstrip("/")
for prefix in ("https://github.com/", "http://github.com/"):
if v.lower().startswith(prefix):
v = v[len(prefix) :]
break
return v
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Normalize repo_name should strip query/fragment parts from full URLs; otherwise owner/repo?tab=readme passes validation and becomes an invalid repo identifier.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/airweave/platform/configs/config.py, line 186:

<comment>Normalize repo_name should strip query/fragment parts from full URLs; otherwise `owner/repo?tab=readme` passes validation and becomes an invalid repo identifier.</comment>

<file context>
@@ -171,6 +173,18 @@ class GitHubConfig(SourceConfig):
+                if v.lower().startswith(prefix):
+                    v = v[len(prefix) :]
+                    break
+        return v
+
     @field_validator("repo_name")
</file context>
Suggested change
return v
return v.split("?", 1)[0].split("#", 1)[0] if isinstance(v, str) else v
Fix with Cubic


@field_validator("repo_name")
@classmethod
def validate_repo_name(cls, v: str) -> str:
Expand Down Expand Up @@ -259,21 +272,20 @@ class GmailConfig(SourceConfig):

@field_validator("included_labels", "excluded_labels", "excluded_categories", mode="before")
@classmethod
def parse_list_fields(cls, value):
def parse_list_fields(cls, value: Any) -> list[str]:
"""Convert comma-separated string to list if needed."""
if isinstance(value, str):
if not value.strip():
return []
return [item.strip() for item in value.split(",") if item.strip()]
return value
return value # type: ignore[no-any-return]

@field_validator("after_date")
@classmethod
def validate_date_format(cls, value):
def validate_date_format(cls, value: Optional[str]) -> Optional[str]:
"""Validate date format and convert to YYYY/MM/DD."""
if not value:
return value
# Accept both YYYY/MM/DD and YYYY-MM-DD formats
return value.replace("-", "/")


Expand Down Expand Up @@ -314,10 +326,10 @@ class GoogleDriveConfig(SourceConfig):

@field_validator("include_patterns", mode="before")
@classmethod
def _parse_include_patterns(cls, value):
def _parse_include_patterns(cls, value: Any) -> list[str]:
if isinstance(value, str):
return [p.strip() for p in value.split(",") if p.strip()]
return value
return value # type: ignore[no-any-return]


class GoogleSlidesConfig(SourceConfig):
Expand Down Expand Up @@ -450,21 +462,20 @@ class OutlookMailConfig(SourceConfig):

@field_validator("included_folders", "excluded_folders", mode="before")
@classmethod
def parse_list_fields(cls, value):
def parse_list_fields(cls, value: Any) -> list[str]:
"""Convert comma-separated string to list if needed."""
if isinstance(value, str):
if not value.strip():
return []
return [item.strip() for item in value.split(",") if item.strip()]
return value
return value # type: ignore[no-any-return]

@field_validator("after_date")
@classmethod
def validate_date_format(cls, value):
def validate_date_format(cls, value: Optional[str]) -> Optional[str]:
"""Validate date format and convert to YYYY/MM/DD."""
if not value:
return value
# Accept both YYYY/MM/DD and YYYY-MM-DD formats
return value.replace("-", "/")


Expand Down Expand Up @@ -506,7 +517,7 @@ class CTTIConfig(SourceConfig):

@field_validator("limit", mode="before")
@classmethod
def parse_limit(cls, value):
def parse_limit(cls, value: Any) -> int:
"""Convert string input to integer if needed."""
if isinstance(value, str):
if not value.strip():
Expand All @@ -515,11 +526,11 @@ def parse_limit(cls, value):
return int(value.strip())
except ValueError as e:
raise ValueError("Limit must be a valid integer") from e
return value
return value # type: ignore[no-any-return]

@field_validator("skip", mode="before")
@classmethod
def parse_skip(cls, value):
def parse_skip(cls, value: Any) -> int:
"""Convert string input to integer if needed."""
if isinstance(value, str):
if not value.strip():
Expand All @@ -537,7 +548,7 @@ def parse_skip(cls, value):
if value < 0:
raise ValueError("Skip must be non-negative")
return int(value)
return value
return value # type: ignore[no-any-return]


class SharePointConfig(SourceConfig):
Expand Down Expand Up @@ -649,14 +660,14 @@ class SalesforceConfig(SourceConfig):

@field_validator("instance_url", mode="before")
@classmethod
def strip_https_prefix(cls, value):
def strip_https_prefix(cls, value: Any) -> Optional[str]:
"""Remove https:// or http:// prefix if present."""
if isinstance(value, str):
if value.startswith("https://"):
return value.replace("https://", "", 1)
elif value.startswith("http://"):
return value.replace("http://", "", 1)
return value
return value # type: ignore[no-any-return]


class TodoistConfig(SourceConfig):
Expand Down Expand Up @@ -800,7 +811,7 @@ class StubConfig(SourceConfig):
mode="before",
)
@classmethod
def parse_weight(cls, value):
def parse_weight(cls, value: Any) -> int:
"""Convert string input to integer if needed."""
if isinstance(value, str):
if not value.strip():
Expand All @@ -809,7 +820,7 @@ def parse_weight(cls, value):
return int(value.strip())
except ValueError as e:
raise ValueError("Weight must be a valid integer") from e
return value
return value # type: ignore[no-any-return]


class IncrementalStubConfig(SourceConfig):
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/lib/validation/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,24 +434,32 @@ export const redirectUrlValidation: FieldValidation<string> = {
};

/**
* Repository name validation (owner/repo format)
* Repository name validation (owner/repo format or full GitHub URL)
*/
export const repoNameValidation: FieldValidation<string> = {
field: 'repo_name',
debounceMs: 500,
showOn: 'change',
validate: (value: string): ValidationResult => {
const trimmed = value.trim();
let trimmed = value.trim().replace(/\/+$/, '');

if (!trimmed) {
return { isValid: true, severity: 'info' };
}

// Strip GitHub URL prefix if present
for (const prefix of ['https://github.com/', 'http://github.com/']) {
if (trimmed.toLowerCase().startsWith(prefix)) {
trimmed = trimmed.slice(prefix.length);
break;
}
}

// Must contain a slash
if (!trimmed.includes('/')) {
return {
isValid: false,
hint: 'Repository must be in owner/repo format (e.g., airweave-ai/airweave)',
hint: 'Use owner/repo format (e.g., airweave-ai/airweave) or a full GitHub URL',
severity: 'warning'
};
}
Expand All @@ -460,7 +468,7 @@ export const repoNameValidation: FieldValidation<string> = {
if (parts.length !== 2) {
return {
isValid: false,
hint: 'Repository must be in owner/repo format (e.g., airweave-ai/airweave)',
hint: 'Use owner/repo format (e.g., airweave-ai/airweave) or a full GitHub URL',
severity: 'warning'
};
}
Expand Down
Loading