From a88ad757433766bc468ad6f60b74c035e96f8133 Mon Sep 17 00:00:00 2001 From: mujacica Date: Thu, 13 Nov 2025 13:01:15 +0100 Subject: [PATCH 1/4] feat(perforce): Add Perforce integration infrastructure and stubs This PR adds the boilerplate and infrastructure for Perforce/Helix Core integration: - Type definitions: Added PERFORCE to ExternalProviders and IntegrationProviderSlug - Model updates: Added Perforce to external actor provider choices - Integration registration: Registered PerforceIntegrationProvider - Feature flag: Added organizations:integrations-perforce - Dependencies: Added p4python package - Stub implementations: Created empty Perforce integration, client, and repository classes with all method signatures All methods are stubbed out with proper type hints and docstrings. Implementation will be added in a follow-up PR. --- pyproject.toml | 2 + src/sentry/conf/server.py | 1 + .../integrations/api/bases/external_actor.py | 1 + .../integrations/models/external_actor.py | 1 + src/sentry/integrations/perforce/__init__.py | 6 + src/sentry/integrations/perforce/client.py | 196 ++++++++++++ .../integrations/perforce/integration.py | 289 ++++++++++++++++++ .../integrations/perforce/repository.py | 93 ++++++ src/sentry/integrations/types.py | 5 + 9 files changed, 594 insertions(+) create mode 100644 src/sentry/integrations/perforce/__init__.py create mode 100644 src/sentry/integrations/perforce/client.py create mode 100644 src/sentry/integrations/perforce/integration.py create mode 100644 src/sentry/integrations/perforce/repository.py diff --git a/pyproject.toml b/pyproject.toml index 82eb583e58956c..a54408b8e4cd71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ dependencies = [ "objectstore-client>=0.0.5", "openai>=1.3.5", "orjson>=3.10.10", + "p4python>=2025.1.2767466", "packaging>=24.1", "parsimonious>=0.10.0", "petname>=2.6", @@ -295,6 +296,7 @@ module = [ "onelogin.saml2.auth.*", "onelogin.saml2.constants.*", "onelogin.saml2.idp_metadata_parser.*", + "P4", "rb.*", "statsd.*", "tokenizers.*", diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index b4cfec04b4a503..bd5a755bc5cb97 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -2148,6 +2148,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: "sentry.integrations.discord.DiscordIntegrationProvider", "sentry.integrations.opsgenie.OpsgenieIntegrationProvider", "sentry.integrations.cursor.integration.CursorAgentIntegrationProvider", + "sentry.integrations.perforce.integration.PerforceIntegrationProvider", ) diff --git a/src/sentry/integrations/api/bases/external_actor.py b/src/sentry/integrations/api/bases/external_actor.py index 2db4778b8c4874..ba32088a09fa2c 100644 --- a/src/sentry/integrations/api/bases/external_actor.py +++ b/src/sentry/integrations/api/bases/external_actor.py @@ -34,6 +34,7 @@ ExternalProviders.SLACK, ExternalProviders.MSTEAMS, ExternalProviders.JIRA_SERVER, + ExternalProviders.PERFORCE, ExternalProviders.CUSTOM, } diff --git a/src/sentry/integrations/models/external_actor.py b/src/sentry/integrations/models/external_actor.py index d4fbb9f07c85f1..8b7849e18a0ff3 100644 --- a/src/sentry/integrations/models/external_actor.py +++ b/src/sentry/integrations/models/external_actor.py @@ -41,6 +41,7 @@ class ExternalActor(ReplicatedRegionModel): (ExternalProviders.GITHUB_ENTERPRISE, IntegrationProviderSlug.GITHUB_ENTERPRISE.value), (ExternalProviders.GITLAB, IntegrationProviderSlug.GITLAB.value), (ExternalProviders.JIRA_SERVER, IntegrationProviderSlug.JIRA_SERVER.value), + (ExternalProviders.PERFORCE, IntegrationProviderSlug.PERFORCE.value), # TODO: do migration to delete this from database (ExternalProviders.CUSTOM, "custom_scm"), ), diff --git a/src/sentry/integrations/perforce/__init__.py b/src/sentry/integrations/perforce/__init__.py new file mode 100644 index 00000000000000..fd9b8237f35e64 --- /dev/null +++ b/src/sentry/integrations/perforce/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from .integration import PerforceIntegration, PerforceIntegrationProvider +from .repository import PerforceRepositoryProvider + +__all__ = ["PerforceIntegration", "PerforceIntegrationProvider", "PerforceRepositoryProvider"] diff --git a/src/sentry/integrations/perforce/client.py b/src/sentry/integrations/perforce/client.py new file mode 100644 index 00000000000000..84b03121121642 --- /dev/null +++ b/src/sentry/integrations/perforce/client.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import logging +from collections.abc import Sequence +from typing import Any + +from sentry.integrations.source_code_management.commit_context import ( + CommitContextClient, + FileBlameInfo, + SourceLineInfo, +) +from sentry.integrations.source_code_management.repository import RepositoryClient +from sentry.models.pullrequest import PullRequest, PullRequestComment +from sentry.models.repository import Repository + +logger = logging.getLogger(__name__) + + +class PerforceClient(RepositoryClient, CommitContextClient): + """ + Client for interacting with Perforce server. + Uses P4Python library to execute P4 commands. + """ + + def __init__( + self, + host: str | None = None, + port: int | str | None = None, + user: str | None = None, + password: str | None = None, + client: str | None = None, + ticket: str | None = None, + ): + self.ticket = ticket + self.host = host or "localhost" + self.port = str(port) if port else "1666" + self.user = user or "" + self.password = password + self.client_name = client + self.base_url = f"p4://{self.host}:{self.port}" + + def _connect(self): + """Create and connect a P4 instance.""" + pass + + def _disconnect(self, p4): + """Disconnect P4 instance.""" + pass + + def check_file(self, repo: Repository, path: str, version: str | None) -> object | None: + """ + Check if a file exists in the depot. + + Args: + repo: Repository object containing depot path (includes stream if specified) + path: File path relative to depot + version: Not used (streams are part of depot_path) + + Returns: + File info dict if exists, None otherwise + """ + return None + + def get_file( + self, repo: Repository, path: str, ref: str | None, codeowners: bool = False + ) -> str: + """ + Get file contents from depot. + + Args: + repo: Repository object (depot_path includes stream if specified) + path: File path + ref: Not used (streams are part of depot_path) + codeowners: Whether this is a CODEOWNERS file + + Returns: + File contents as string + """ + return "" + + def _build_depot_path(self, repo: Repository, path: str, ref: str | None = None) -> str: + """ + Build full depot path from repo config and file path. + + Args: + repo: Repository object + path: File path (may include @revision syntax like "file.cpp@42") + ref: Optional ref/revision (for compatibility, but Perforce uses @revision in path) + + Returns: + Full depot path with @revision preserved if present + """ + return "" + + def get_blame( + self, repo: Repository, path: str, ref: str | None = None, lineno: int | None = None + ) -> list[dict[str, Any]]: + """ + Get blame/annotate information for a file (like git blame). + + Uses 'p4 filelog' + 'p4 describe' which is much faster than 'p4 annotate'. + Returns the most recent changelist that modified the file and its author. + This is used for CODEOWNERS-style ownership detection. + + Args: + repo: Repository object (depot_path includes stream if specified) + path: File path relative to depot (may include @revision like "file.cpp@42") + ref: Optional revision/changelist number (appended as @ref if not in path) + lineno: Specific line number to blame (optional, currently ignored) + + Returns: + List with a single entry containing: + - changelist: changelist number + - user: username who made the change + - date: date of change + - description: changelist description + """ + return [] + + def get_depot_info(self) -> dict[str, Any]: + """ + Get server info for testing connection. + + Returns: + Server info dictionary + """ + return {} + + def get_depots(self) -> list[dict[str, Any]]: + """ + List all depots accessible to the user. + + Returns: + List of depot info dictionaries + """ + return [] + + def get_changes( + self, depot_path: str, max_changes: int = 20, start_cl: str | None = None + ) -> list[dict[str, Any]]: + """ + Get changelists for a depot path. + + Args: + depot_path: Depot path (e.g., //depot/main/...) + max_changes: Maximum number of changes to return + start_cl: Starting changelist number + + Returns: + List of changelist dictionaries + """ + return [] + + def get_blame_for_files( + self, files: Sequence[SourceLineInfo], extra: dict[str, Any] + ) -> list[FileBlameInfo]: + """ + Get blame information for multiple files using p4 filelog. + + Uses 'p4 filelog' + 'p4 describe' which is much faster than 'p4 annotate'. + Returns the most recent changelist that modified each file. + + Note: This does not provide line-specific blame. It returns the most recent + changelist for the entire file, which is sufficient for suspect commit detection. + + Returns a list of FileBlameInfo objects containing commit details for each file. + """ + return [] + + def create_comment(self, repo: str, issue_id: str, data: dict[str, Any]) -> Any: + """Create comment. Not applicable for Perforce.""" + raise NotImplementedError("Perforce does not support issue comments") + + def update_comment( + self, repo: str, issue_id: str, comment_id: str, data: dict[str, Any] + ) -> Any: + """Update comment. Not applicable for Perforce.""" + raise NotImplementedError("Perforce does not support issue comments") + + def create_pr_comment(self, repo: Repository, pr: PullRequest, data: dict[str, Any]) -> Any: + """Create PR comment. Not applicable for Perforce.""" + raise NotImplementedError("Perforce does not have native pull requests") + + def update_pr_comment( + self, + repo: Repository, + pr: PullRequest, + pr_comment: PullRequestComment, + data: dict[str, Any], + ) -> Any: + """Update PR comment. Not applicable for Perforce.""" + raise NotImplementedError("Perforce does not have native pull requests") + + def get_merge_commit_sha_from_commit(self, repo: Repository, sha: str) -> str | None: + """Get merge commit. Not applicable for Perforce.""" + return None diff --git a/src/sentry/integrations/perforce/integration.py b/src/sentry/integrations/perforce/integration.py new file mode 100644 index 00000000000000..552fb57133d760 --- /dev/null +++ b/src/sentry/integrations/perforce/integration.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import logging +from collections.abc import Mapping, MutableMapping, Sequence +from typing import Any + +from django.utils.translation import gettext_lazy as _ + +from sentry.integrations.base import ( + FeatureDescription, + IntegrationData, + IntegrationFeatures, + IntegrationMetadata, + IntegrationProvider, +) +from sentry.integrations.models.integration import Integration +from sentry.integrations.perforce.client import PerforceClient +from sentry.integrations.services.repository import RpcRepository +from sentry.integrations.source_code_management.commit_context import CommitContextIntegration +from sentry.integrations.source_code_management.repository import RepositoryIntegration +from sentry.models.repository import Repository +from sentry.organizations.services.organization.model import RpcOrganization +from sentry.pipeline.views.base import PipelineView +from sentry.shared_integrations.exceptions import ApiError + +logger = logging.getLogger(__name__) + +DESCRIPTION = """ +Connect your Sentry organization to your Perforce/Helix Core server to enable +stacktrace linking, commit tracking, suspect commit detection, and code ownership. +View source code directly from error stack traces, identify suspect commits that +may have introduced issues, and automatically determine code owners using Perforce +annotate (blame) information. +""" + +FEATURES = [ + FeatureDescription( + """ + Link your Sentry stack traces back to your Perforce depot files with support + for P4Web and Helix Swarm web viewers. Automatically maps error locations to + source code using configurable code mappings. + """, + IntegrationFeatures.STACKTRACE_LINK, + ), + FeatureDescription( + """ + Track commits and changelists from your Perforce depots. Browse and add + depots to your Sentry projects for comprehensive source code integration. + Suspect commits are automatically identified by analyzing which changelists + modified the code where errors occur. + """, + IntegrationFeatures.COMMITS, + ), + FeatureDescription( + """ + Import your Perforce CODEOWNERS file and use it alongside your ownership rules + to assign Sentry issues. Uses Perforce annotate to identify code owners based + on who last modified each line. + """, + IntegrationFeatures.CODEOWNERS, + ), +] + +metadata = IntegrationMetadata( + description=DESCRIPTION.strip(), + features=FEATURES, + author="Sentry", + noun=_("Installation"), + issue_url="https://github.com/getsentry/sentry/issues", + source_url="https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/perforce", + aspects={ + "alerts": [], + "configure_integration": {"title": "Configure your Perforce server connection"}, + }, +) + + +class PerforceIntegration(RepositoryIntegration, CommitContextIntegration): + """ + Integration for Perforce/Helix Core version control system. + Provides stacktrace linking to depot files and suspect commit detection. + """ + + integration_name = "perforce" + codeowners_locations = ["CODEOWNERS", ".perforce/CODEOWNERS", "docs/CODEOWNERS"] + + def __init__( + self, + model: Integration, + organization_id: int, + ): + super().__init__(model=model, organization_id=organization_id) + self._client: PerforceClient | None = None + + def get_client(self) -> PerforceClient: + """Get the Perforce client instance.""" + pass + + def on_create_or_update_comment_error(self, api_error: ApiError, metrics_base: str) -> bool: + """ + Handle errors from PR comment operations. + Perforce doesn't have native pull requests, so this always returns False. + """ + return False + + def source_url_matches(self, url: str) -> bool: + """Check if URL is from this Perforce server.""" + return False + + def matches_repository_depot_path(self, repo: Repository, filepath: str) -> bool: + """ + Check if a file path matches this repository's depot path. + + When SRCSRV transformers remap paths to absolute depot paths (e.g., + //depot/project/src/file.cpp), this method verifies that the depot path + matches the repository's configured depot_path. + + Args: + repo: Repository object + filepath: File path (may be absolute depot path or relative path) + + Returns: + True if the filepath matches this repository's depot + """ + return False + + def check_file(self, repo: Repository, filepath: str, branch: str | None = None) -> str | None: + """ + Check if a filepath belongs to this Perforce repository and return the URL. + + Perforce doesn't have a REST API to check file existence, so we just + verify the filepath matches the depot_path configuration and return + the formatted URL. + + Args: + repo: Repository object + filepath: File path (may be absolute depot path or relative path) + branch: Branch/stream name (optional) + + Returns: + Formatted URL if the filepath matches this repository, None otherwise + """ + return None + + def format_source_url(self, repo: Repository, filepath: str, branch: str | None) -> str: + """ + Format source URL for stacktrace linking. + + The Symbolic transformer includes revision info directly in the filepath + using Perforce's native @revision syntax (e.g., "processor.cpp@42"). + + Args: + repo: Repository object + filepath: File path, may include @revision (e.g., "app/file.cpp@42") + branch: Stream name (e.g., "main", "dev") to be inserted after depot path. + For Perforce streams: //depot/stream/path/to/file + + Returns: + Formatted URL (p4:// or web viewer URL) + """ + return "" + + def extract_branch_from_source_url(self, repo: Repository, url: str) -> str: + """ + Extract branch/stream from URL. + For Perforce, streams are part of the depot path, not separate refs. + Returns empty string as we don't use branch refs. + """ + return "" + + def extract_source_path_from_source_url(self, repo: Repository, url: str) -> str: + """ + Extract file path from URL, removing revision specifiers. + + Handles URLs with revisions like: + - p4://depot/path/file.cpp#42 + - https://swarm/files//depot/path/file.cpp?v=42 + + Returns just the file path without revision info. + """ + return "" + + def get_repositories( + self, query: str | None = None, page_number_limit: int | None = None + ) -> list[dict[str, Any]]: + """ + Get list of depots/streams from Perforce server. + + Returns: + List of repository dictionaries + """ + return [] + + def has_repo_access(self, repo: RpcRepository) -> bool: + """Check if integration can access the depot.""" + return False + + def get_unmigratable_repositories(self) -> list[RpcRepository]: + """Get repositories that can't be migrated. Perforce doesn't need migration.""" + return [] + + def test_connection(self) -> dict[str, Any]: + """ + Test the Perforce connection with current credentials. + + Returns: + Dictionary with connection status and server info + """ + return {} + + def get_organization_config(self) -> list[dict[str, Any]]: + """ + Get configuration form fields for organization-level settings. + These fields will be displayed in the integration settings UI. + """ + return [] + + def update_organization_config(self, data: MutableMapping[str, Any]) -> None: + """ + Update organization config and optionally validate credentials. + Only tests connection when password or ticket is changed to avoid annoying + validations on every field blur. + """ + pass + + +class PerforceIntegrationProvider(IntegrationProvider): + """Provider for Perforce integration.""" + + key = "perforce" + name = "Perforce" + metadata = metadata + integration_cls = PerforceIntegration + features = frozenset( + [ + IntegrationFeatures.STACKTRACE_LINK, + IntegrationFeatures.COMMITS, + IntegrationFeatures.CODEOWNERS, + ] + ) + + def get_pipeline_views(self) -> Sequence[PipelineView]: + """Get pipeline views for installation flow.""" + return [PerforceInstallationView()] + + def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: + """ + Build integration data from installation state. + + Args: + state: Installation state from pipeline + + Returns: + Integration data dictionary + """ + return {} + + def post_install( + self, + integration: Integration, + organization: RpcOrganization, + *, + extra: dict[str, Any], + ) -> None: + """Actions after installation.""" + pass + + def setup(self) -> None: + """Setup integration provider.""" + pass + + +class PerforceInstallationView: + """ + Installation view for Perforce configuration. + + This is a simple pass-through view. The actual configuration + happens in the Settings tab after installation via get_organization_config(). + """ + + def dispatch(self, request, pipeline): + """ + Handle installation request. + + Since Perforce doesn't use OAuth and configuration is done through + the Settings form, we just pass through to create the integration. + Users will configure P4 server details in the Settings tab. + """ + pass diff --git a/src/sentry/integrations/perforce/repository.py b/src/sentry/integrations/perforce/repository.py new file mode 100644 index 00000000000000..a744043685da90 --- /dev/null +++ b/src/sentry/integrations/perforce/repository.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import logging +from collections.abc import Mapping, Sequence +from typing import Any + +from sentry.models.organization import Organization +from sentry.models.pullrequest import PullRequest +from sentry.models.repository import Repository +from sentry.organizations.services.organization.model import RpcOrganization +from sentry.plugins.providers import IntegrationRepositoryProvider +from sentry.plugins.providers.integration_repository import RepositoryConfig + +logger = logging.getLogger(__name__) + + +class PerforceRepositoryProvider(IntegrationRepositoryProvider): + """Repository provider for Perforce integration.""" + + name = "Perforce" + repo_provider = "perforce" + + def get_repository_data( + self, organization: Organization, config: dict[str, Any] + ) -> Mapping[str, Any]: + """ + Validate and return repository data. + + Args: + organization: Organization instance + config: Repository configuration from user + + Returns: + Repository configuration dictionary + """ + return {} + + def build_repository_config( + self, organization: RpcOrganization, data: dict[str, Any] + ) -> RepositoryConfig: + """ + Build repository configuration for database storage. + + Args: + organization: Organization RPC object + data: Repository data + + Returns: + Repository configuration + """ + return {} + + def compare_commits( + self, repo: Repository, start_sha: str | None, end_sha: str + ) -> Sequence[Mapping[str, Any]]: + """ + Compare commits (changelists) between two versions. + + Args: + repo: Repository instance + start_sha: Starting changelist number (or None for initial) + end_sha: Ending changelist number + + Returns: + List of changelist dictionaries + """ + return [] + + def _format_commits( + self, changelists: list[dict[str, Any]], depot_path: str + ) -> Sequence[Mapping[str, Any]]: + """ + Format Perforce changelists into Sentry commit format. + + Args: + changelists: List of changelist dictionaries from P4 + depot_path: Depot path + + Returns: + List of commits in Sentry format + """ + return [] + + def pull_request_url(self, repo: Repository, pull_request: PullRequest) -> str: + """ + Get URL for pull request. + Perforce doesn't have native PRs, but might integrate with Swarm. + """ + return "" + + def repository_external_slug(self, repo: Repository) -> str: + """Get external slug for repository.""" + return "" diff --git a/src/sentry/integrations/types.py b/src/sentry/integrations/types.py index 90b5a249bf1bf2..be119b0a36be63 100644 --- a/src/sentry/integrations/types.py +++ b/src/sentry/integrations/types.py @@ -19,6 +19,7 @@ class ExternalProviders(ValueEqualityEnum): GITHUB_ENTERPRISE = 201 GITLAB = 210 JIRA_SERVER = 300 + PERFORCE = 400 # TODO: do migration to delete this from database CUSTOM = 700 @@ -42,6 +43,7 @@ class IntegrationProviderSlug(StrEnum): BITBUCKET_SERVER = "bitbucket_server" PAGERDUTY = "pagerduty" OPSGENIE = "opsgenie" + PERFORCE = "perforce" class DataForwarderProviderSlug(StrEnum): @@ -62,6 +64,7 @@ class ExternalProviderEnum(StrEnum): GITHUB_ENTERPRISE = IntegrationProviderSlug.GITHUB_ENTERPRISE GITLAB = IntegrationProviderSlug.GITLAB JIRA_SERVER = IntegrationProviderSlug.JIRA_SERVER + PERFORCE = IntegrationProviderSlug.PERFORCE EXTERNAL_PROVIDERS_REVERSE = { @@ -74,6 +77,7 @@ class ExternalProviderEnum(StrEnum): ExternalProviderEnum.GITHUB: ExternalProviders.GITHUB, ExternalProviderEnum.GITHUB_ENTERPRISE: ExternalProviders.GITHUB_ENTERPRISE, ExternalProviderEnum.GITLAB: ExternalProviders.GITLAB, + ExternalProviderEnum.PERFORCE: ExternalProviders.PERFORCE, ExternalProviderEnum.CUSTOM: ExternalProviders.CUSTOM, } @@ -90,6 +94,7 @@ class ExternalProviderEnum(StrEnum): ExternalProviders.GITHUB_ENTERPRISE: ExternalProviderEnum.GITHUB_ENTERPRISE.value, ExternalProviders.GITLAB: ExternalProviderEnum.GITLAB.value, ExternalProviders.JIRA_SERVER: ExternalProviderEnum.JIRA_SERVER.value, + ExternalProviders.PERFORCE: ExternalProviderEnum.PERFORCE.value, ExternalProviders.CUSTOM: ExternalProviderEnum.CUSTOM.value, } From a39995e6239ce9e43bc5701ab3d60885a251aaef Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:03:03 +0000 Subject: [PATCH 2/4] :snowflake: re-freeze requirements --- uv.lock | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/uv.lock b/uv.lock index cf666eb7048b98..693c6ab626c22b 100644 --- a/uv.lock +++ b/uv.lock @@ -1264,6 +1264,16 @@ wheels = [ { url = "https://pypi.devinfra.sentry.io/wheels/outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5" }, ] +[[package]] +name = "p4python" +version = "2025.1.2767466" +source = { registry = "https://pypi.devinfra.sentry.io/simple" } +wheels = [ + { url = "https://pypi.devinfra.sentry.io/wheels/p4python-2025.1.2767466-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4ffa3f8a586c6a230b98abfb614cef3066484e7dc80581dfaff6b408329d29a" }, + { url = "https://pypi.devinfra.sentry.io/wheels/p4python-2025.1.2767466-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:6e0262d99ec0e3af9ada4c977914d95775dad36be811aa6eebec6083e75ff7b6" }, + { url = "https://pypi.devinfra.sentry.io/wheels/p4python-2025.1.2767466-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6a682521bc25f33f8aab6a33f49d9f81be4a2192dd1a36aeb8eeeadd396eb0cf" }, +] + [[package]] name = "packaging" version = "24.1" @@ -1972,6 +1982,7 @@ dependencies = [ { name = "objectstore-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "orjson", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "p4python", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "parsimonious", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "petname", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2138,6 +2149,7 @@ requires-dist = [ { name = "objectstore-client", specifier = ">=0.0.5" }, { name = "openai", specifier = ">=1.3.5" }, { name = "orjson", specifier = ">=3.10.10" }, + { name = "p4python", specifier = ">=2025.1.2767466" }, { name = "packaging", specifier = ">=24.1" }, { name = "parsimonious", specifier = ">=0.10.0" }, { name = "petname", specifier = ">=2.6" }, From 19cd1a88e3d5a6a47949fe5a3781638c72c0e5b3 Mon Sep 17 00:00:00 2001 From: mujacica Date: Thu, 13 Nov 2025 13:09:30 +0100 Subject: [PATCH 3/4] Move code mapping change to integration --- .../endpoints/organization_code_mappings.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/sentry/integrations/api/endpoints/organization_code_mappings.py b/src/sentry/integrations/api/endpoints/organization_code_mappings.py index 9bcb1dd8aef8da..793bd64f3e503a 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mappings.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mappings.py @@ -42,7 +42,8 @@ class RepositoryProjectPathConfigSerializer(CamelSnakeModelSerializer): source_root = gen_path_regex_field() default_branch = serializers.RegexField( r"^(^(?![\/]))([\w\.\/-]+)(? Date: Thu, 13 Nov 2025 13:20:07 +0100 Subject: [PATCH 4/4] Fix mypy errors --- src/sentry/integrations/perforce/integration.py | 4 ++-- src/sentry/integrations/perforce/repository.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentry/integrations/perforce/integration.py b/src/sentry/integrations/perforce/integration.py index 552fb57133d760..26af17f2be357e 100644 --- a/src/sentry/integrations/perforce/integration.py +++ b/src/sentry/integrations/perforce/integration.py @@ -94,7 +94,7 @@ def __init__( def get_client(self) -> PerforceClient: """Get the Perforce client instance.""" - pass + raise NotImplementedError def on_create_or_update_comment_error(self, api_error: ApiError, metrics_base: str) -> bool: """ @@ -253,7 +253,7 @@ def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: Returns: Integration data dictionary """ - return {} + return {"external_id": ""} def post_install( self, diff --git a/src/sentry/integrations/perforce/repository.py b/src/sentry/integrations/perforce/repository.py index a744043685da90..b084ca707a0c61 100644 --- a/src/sentry/integrations/perforce/repository.py +++ b/src/sentry/integrations/perforce/repository.py @@ -48,7 +48,7 @@ def build_repository_config( Returns: Repository configuration """ - return {} + raise NotImplementedError def compare_commits( self, repo: Repository, start_sha: str | None, end_sha: str