Source Code Management Platform
The SCM (Source Code Management) platform is a vendor-agnostic abstraction layer for interacting with source code management providers such as GitHub, GitLab, and Bitbucket. It decouples Sentry's product features from provider-specific APIs by presenting a single, declarative interface for both reading and writing SCM resources and for reacting to SCM webhook events.
- Provider independence. Product code should never import a provider's client or parse a provider's response format directly. All interaction flows through a common type system so that adding a new provider does not require changes to callers.
- Declarative usage. Callers describe what they want (e.g. "create a pull request") not how to accomplish it. Initialization, authentication, rate limiting, and response mapping are handled internally.
- Observability by default. Every outbound action and every inbound webhook listener automatically records success/failure metrics, emits traces, and reports errors to Sentry. Callers do not need to instrument their own usage.
- Fair access. Referrer-based rate limiting with allocation policies prevents any single caller from exhausting a provider's API budget. Shared and caller-specific quotas are enforced transparently.
- Centrally enforced access controls. Access controls must be strictly and consistently enforced across all SCM providers to prevent unprivileged access to sensitive customer data, ensuring the security model is implemented once and applied universally.
The platform exposes three subsystems:
- Actions — outbound operations initiated by Sentry code. The
SourceCodeManagerclass provides 70+ methods covering comments, reactions, pull requests, branches, git objects, reviews, and check runs. - Actions RPC — the same
SourceCodeManagerinterface exposed over the network, enabling use from services outside the monolith. - Event Stream — inbound webhook processing. SCM providers push events which are deserialized into typed, provider-neutral dataclasses (
CheckRunEvent,CommentEvent,PullRequestEvent) and dispatched to registered listener functions.
Import SourceCodeManager from the scm module and initialize it with a repository ID:
from scm.actions import SourceCodeManager
scm = SourceCodeManager.make_from_repository_id(organization_id=1, repository_id=2)
from scm.actions import SourceCodeManager
scm = SourceCodeManager.make_from_repository_id(organization_id=1, repository_id=2)
This scopes the instance to what the repository's provider can offer.
Import the actions your use case requires:
from scm.actions import SourceCodeManager, create_issue_reaction, create_issue_comment
from scm.actions import SourceCodeManager, create_issue_reaction, create_issue_comment
By default the SourceCodeManager cannot execute methods without a capability assertion. Use isinstance guards to assert that the provider supports the action you need:
from scm.types import CreateIssueReactionProtocol, CreateIssueCommentProtocol
if isinstance(scm, CreateIssueReactionProtocol):
create_issue_reaction(scm, issue_id="1", reaction="eyes")
elif isinstance(scm, CreateIssueCommentProtocol):
create_issue_comment(scm, issue_id="1", body="We've seen your request.")
else:
return None # Unsupported provider — do nothing.
from scm.types import CreateIssueReactionProtocol, CreateIssueCommentProtocol
if isinstance(scm, CreateIssueReactionProtocol):
create_issue_reaction(scm, issue_id="1", reaction="eyes")
elif isinstance(scm, CreateIssueCommentProtocol):
create_issue_comment(scm, issue_id="1", body="We've seen your request.")
else:
return None # Unsupported provider — do nothing.
Capabilities can be composed when granularity is not required:
class GitInteractionProtocol(
GetTreeProtocol,
GetGitCommitProtocol,
CreateGitBlobProtocol,
CreateGitTreeProtocol,
CreateGitCommitProtocol,
):
...
if isinstance(scm, GitInteractionProtocol):
# do work
...
class GitInteractionProtocol(
GetTreeProtocol,
GetGitCommitProtocol,
CreateGitBlobProtocol,
CreateGitTreeProtocol,
CreateGitCommitProtocol,
):
...
if isinstance(scm, GitInteractionProtocol):
# do work
...
If you need to target a specific provider directly, you may — but this is discouraged:
from scm.providers.github.provider import GitHubProvider
if isinstance(scm, GitHubProvider):
# GitHub-specific work
...
from scm.providers.github.provider import GitHubProvider
if isinstance(scm, GitHubProvider):
# GitHub-specific work
...
Prefer capability-based checks so your feature is automatically available to new providers.
The SourceCodeManager is fully accessible over RPC from services outside the monolith:
from scm.rpc.client import SourceCodeManager
scm = SourceCodeManager.make_from_repository_id(
organization_id=1,
repository_id=("github", "owner/repo"),
base_url="http://127.0.0.1:8080",
signing_secret="secret",
)
if isinstance(scm, CreateIssueReactionProtocol):
try:
create_issue_reaction(scm, issue_id="1", reaction="+1")
except SCMError:
retry_this_action(...)
from scm.rpc.client import SourceCodeManager
scm = SourceCodeManager.make_from_repository_id(
organization_id=1,
repository_id=("github", "owner/repo"),
base_url="http://127.0.0.1:8080",
signing_secret="secret",
)
if isinstance(scm, CreateIssueReactionProtocol):
try:
create_issue_reaction(scm, issue_id="1", reaction="+1")
except SCMError:
retry_this_action(...)
The RPC client implements the same protocol interfaces as the in-process client, so all isinstance guards and action functions work identically.
All SCM actions raise exceptions on failure. Every exception is a subclass of SCMError:
from scm.errors import SCMError
try:
create_issue_reaction(scm, issue_id="1", reaction="+1")
except SCMError:
retry_this_action(...)
from scm.errors import SCMError
try:
create_issue_reaction(scm, issue_id="1", reaction="+1")
except SCMError:
retry_this_action(...)
List endpoints return a typed dict result. Use PaginationParams to traverse pages:
from scm.types import PaginationParams
page1 = get_issue_comments(scm, issue_id="1")
cursor = page1["meta"]["next_cursor"]
if cursor:
page2 = get_issue_comments(scm, issue_id="1", pagination=PaginationParams(cursor=cursor, per_page=50))
from scm.types import PaginationParams
page1 = get_issue_comments(scm, issue_id="1")
cursor = page1["meta"]["next_cursor"]
if cursor:
page2 = get_issue_comments(scm, issue_id="1", pagination=PaginationParams(cursor=cursor, per_page=50))
cursor: opaque token from the previous page'snext_cursorper_page: number of items per pagenext_cursorisNonewhen there are no more pages
SCM providers push events to Sentry. Register typed listeners using the @scm_event_stream.listen_for decorator:
from sentry.scm.stream import CheckRunEvent, scm_event_stream
@scm_event_stream.listen_for(event_type="check_run")
def listen_for_check_run(event: CheckRunEvent):
# do work
return None
from sentry.scm.stream import CheckRunEvent, scm_event_stream
@scm_event_stream.listen_for(event_type="check_run")
def listen_for_check_run(event: CheckRunEvent):
# do work
return None
Listeners are isolated and run asynchronously in their own worker processes.
Supported event types:
CheckRunEventCommentEventPullRequestEvent
For the full list of available actions and their signatures, see src/scm/actions.py.
Both subsystems emit metrics under the sentry.scm namespace:
| Metric | Source |
|---|---|
sentry.scm.actions.success | Every successful outbound action (tagged by provider and referrer) |
sentry.scm.actions.failed | Unhandled exception during an outbound action |
sentry.scm.produce_event_to_scm_stream.success | Event successfully dispatched to listeners |
sentry.scm.produce_event_to_scm_stream.failed | Dispatch failure (tagged by reason) |
sentry.scm.run_listener.success | Listener executed successfully (tagged by listener name) |
sentry.scm.run_listener.failed | Listener failed (tagged by reason and listener name) |
sentry.scm.run_listener.message.size | Serialized event size in bytes |
sentry.scm.run_listener.queue_time | Time from webhook receipt to task start |
sentry.scm.run_listener.task_time | Time to execute the listener |
sentry.scm.run_listener.real_time | End-to-end time from webhook receipt to listener completion |
The scm-platform repo includes CLI scripts for running an RPC server and client locally against the real GitHub API.
# Populate .credentials with your GitHub App credentials (KEY=VALUE, one per line):
# GITHUB_APP_ID=<id>
# GITHUB_PRIVATE_KEY_PATH=<path-to-.pem>
# GITHUB_INSTALLATION_ID=<id>
# SCM_RPC_SIGNING_SECRET=secret
bin/github-server
# or override inline:
bin/github-server --app-id 12345 --private-key key.pem --installation-id 67890 --port 8080
# Populate .credentials with your GitHub App credentials (KEY=VALUE, one per line):
# GITHUB_APP_ID=<id>
# GITHUB_PRIVATE_KEY_PATH=<path-to-.pem>
# GITHUB_INSTALLATION_ID=<id>
# SCM_RPC_SIGNING_SECRET=secret
bin/github-server
# or override inline:
bin/github-server --app-id 12345 --private-key key.pem --installation-id 67890 --port 8080
See Creating a GitHub App for how to obtain App ID, private key, and installation ID.
Supported clients:
bin/github-client— GitHubbin/gitlab-client— GitLab
bin/github-client --repo owner/repo get-pull-request 42
bin/github-client --repo owner/repo create-issue-comment 10 "Hello from the RPC client"
bin/github-client --repo owner/repo get-pull-request 42
bin/github-client --repo owner/repo create-issue-comment 10 "Hello from the RPC client"
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").