chore(eco): Adds new CLI command for creating projects#111766
chore(eco): Adds new CLI command for creating projects#111766GabeVillalobos merged 5 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: CLI skips most default project settings on creation
- The CLI now calls
apply_default_project_settings(organization, project)instead of onlyset_default_symbol_sources(project), aligning new project defaults with API-created projects and adding a regression test for JavaScript inbound filters.
- The CLI now calls
Or push these changes by commenting:
@cursor push db3eb7a965
Preview (db3eb7a965)
diff --git a/src/sentry/runner/commands/createproject.py b/src/sentry/runner/commands/createproject.py
new file mode 100644
--- /dev/null
+++ b/src/sentry/runner/commands/createproject.py
@@ -1,0 +1,83 @@
+from __future__ import annotations
+
+import click
+
+from sentry.runner.decorators import configuration
+
+
+def _resolve_organization(org_value: str):
+ from sentry.models.organization import Organization
+
+ try:
+ if org_value.isdigit():
+ return Organization.objects.get(id=int(org_value))
+ return Organization.objects.get(slug=org_value)
+ except Organization.DoesNotExist:
+ raise click.ClickException(f"Organization not found: {org_value}")
+
+
+@click.command()
+@click.option("--name", required=True, help="The name of the project.")
+@click.option(
+ "--platform",
+ required=True,
+ help="The platform for the project (e.g., 'python', 'javascript-react').",
+)
+@click.option("--organization", "org", required=True, help="Organization ID or slug.")
+@click.option("--team", default=None, help="Team slug to grant access to the project.")
+@configuration
+def createproject(
+ name: str,
+ platform: str,
+ org: str,
+ team: str | None,
+) -> None:
+ """Create a new project."""
+ from django.db import router, transaction
+
+ from sentry.core.endpoints.team_projects import apply_default_project_settings
+ from sentry.models.project import Project
+ from sentry.models.projectkey import ProjectKey
+ from sentry.models.team import Team, TeamStatus
+ from sentry.signals import project_created
+
+ organization = _resolve_organization(org)
+
+ team_instance = None
+ if team:
+ try:
+ team_instance = Team.objects.get(
+ organization=organization,
+ slug=team,
+ status=TeamStatus.ACTIVE,
+ )
+ except Team.DoesNotExist:
+ raise click.ClickException(
+ f"Team not found: '{team}' in organization '{organization.slug}'"
+ )
+
+ with transaction.atomic(router.db_for_write(Project)):
+ project = Project.objects.create(
+ name=name,
+ organization=organization,
+ platform=platform,
+ )
+
+ if team_instance:
+ project.add_team(team_instance)
+
+ apply_default_project_settings(organization, project)
+
+ project_created.send_robust(
+ project=project,
+ default_rules=True,
+ sender=createproject,
+ )
+
+ key = ProjectKey.get_default(project)
+ dsn = key.dsn_public if key else "(no DSN available)"
+
+ click.echo("Created project:")
+ click.echo(f" ID: {project.id}")
+ click.echo(f" Slug: {project.slug}")
+ click.echo(f" DSN: {dsn}")
diff --git a/src/sentry/runner/main.py b/src/sentry/runner/main.py
--- a/src/sentry/runner/main.py
+++ b/src/sentry/runner/main.py
@@ -45,6 +45,7 @@
"sentry.runner.commands.configoptions.configoptions",
"sentry.runner.commands.createflag.createflag",
"sentry.runner.commands.createflag.createissueflag",
+ "sentry.runner.commands.createproject.createproject",
"sentry.runner.commands.createuser.createuser",
"sentry.runner.commands.devserver.devserver",
"sentry.runner.commands.django.django",
diff --git a/tests/sentry/runner/commands/test_createproject.py b/tests/sentry/runner/commands/test_createproject.py
new file mode 100644
--- /dev/null
+++ b/tests/sentry/runner/commands/test_createproject.py
@@ -1,0 +1,74 @@
+from sentry.ingest import inbound_filters
+from sentry.models.project import Project
+from sentry.models.projectkey import ProjectKey
+from sentry.runner.commands.createproject import createproject
+from sentry.testutils.cases import CliTestCase
+
+
+class CreateProjectTest(CliTestCase):
+ command = createproject
+ default_args: list[str] = []
+
+ def test_basic_creation_with_org_slug(self) -> None:
+ org = self.create_organization(name="test-org")
+ rv = self.invoke("--name=My Project", "--platform=python", f"--organization={org.slug}")
+ assert rv.exit_code == 0, rv.output
+
+ project = Project.objects.get(organization=org, name="My Project")
+ assert project.platform == "python"
+ assert f"ID: {project.id}" in rv.output
+ assert f"Slug: {project.slug}" in rv.output
+
+ key = ProjectKey.get_default(project)
+ assert key is not None
+ assert f"DSN: {key.dsn_public}" in rv.output
+
+ def test_basic_creation_with_org_id(self) -> None:
+ org = self.create_organization(name="test-org")
+ rv = self.invoke("--name=My Project", "--platform=python", f"--organization={org.id}")
+ assert rv.exit_code == 0, rv.output
+
+ project = Project.objects.get(organization=org, name="My Project")
+ assert project.platform == "python"
+
+ def test_with_team(self) -> None:
+ org = self.create_organization(name="test-org")
+ team = self.create_team(organization=org, slug="backend")
+ rv = self.invoke(
+ "--name=My Project",
+ "--platform=javascript-react",
+ f"--organization={org.slug}",
+ f"--team={team.slug}",
+ )
+ assert rv.exit_code == 0, rv.output
+
+ project = Project.objects.get(organization=org, name="My Project")
+ assert team in project.teams.all()
+
+ def test_javascript_project_gets_default_inbound_filters(self) -> None:
+ org = self.create_organization(name="test-org")
+ rv = self.invoke(
+ "--name=My Project",
+ "--platform=javascript-react",
+ f"--organization={org.slug}",
+ )
+ assert rv.exit_code == 0, rv.output
+
+ project = Project.objects.get(organization=org, name="My Project")
+ assert inbound_filters.get_filter_state("browser-extensions", project)
+
+ def test_invalid_organization(self) -> None:
+ rv = self.invoke("--name=My Project", "--platform=python", "--organization=nonexistent")
+ assert rv.exit_code != 0
+ assert "Organization not found" in rv.output
+
+ def test_invalid_team(self) -> None:
+ org = self.create_organization(name="test-org")
+ rv = self.invoke(
+ "--name=My Project",
+ "--platform=python",
+ f"--organization={org.slug}",
+ "--team=nonexistent",
+ )
+ assert rv.exit_code != 0
+ assert "Team not found" in rv.outputThis Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
8959e06 to
ccf878e
Compare
| @click.option( | ||
| "--platform", | ||
| required=True, | ||
| help="The platform for the project (e.g., 'python', 'javascript-react').", |
There was a problem hiding this comment.
I don't know if this is required for a new project, so maybe optional?
Also this might be okay to skip but just calling out that we aren't validating that the platform text matches an existing expected sdk names, e.g. --platform=JS vs --platform=javascript
There was a problem hiding this comment.
Yeah that's true, let me refine this a bit and handle the cursor comment above. Thanks for taking a look in the meantime!
a579807 to
4fff575
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| if team_instance: | ||
| project.add_team(team_instance) | ||
|
|
||
| set_default_symbol_sources(project) |
There was a problem hiding this comment.
Redundant duplicate call to set_default_symbol_sources
Low Severity
set_default_symbol_sources(project) is called explicitly on line 74, and then apply_default_project_settings(organization, project) is called on line 75 which internally also calls set_default_symbol_sources(project, organization). This results in the symbol sources being written to the database twice for every project creation. The explicit call is redundant and can be removed.



Adds a new CLI command to create projects, attached to the given organization