Skip to content

chore(eco): Adds new CLI command for creating projects#111766

Merged
GabeVillalobos merged 5 commits intomasterfrom
worktree-add-project-create-cli-command
Mar 30, 2026
Merged

chore(eco): Adds new CLI command for creating projects#111766
GabeVillalobos merged 5 commits intomasterfrom
worktree-add-project-create-cli-command

Conversation

@GabeVillalobos
Copy link
Copy Markdown
Member

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

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Mar 27, 2026
@GabeVillalobos GabeVillalobos requested review from a team March 27, 2026 23:55
@GabeVillalobos GabeVillalobos marked this pull request as ready for review March 30, 2026 16:36
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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 only set_default_symbol_sources(project), aligning new project defaults with API-created projects and adding a regression test for JavaScript inbound filters.

Create PR

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.output

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread src/sentry/runner/commands/createproject.py
@GabeVillalobos GabeVillalobos force-pushed the worktree-add-project-create-cli-command branch from 8959e06 to ccf878e Compare March 30, 2026 16:46
Comment thread src/sentry/runner/commands/createproject.py Outdated
Comment thread src/sentry/runner/commands/createproject.py
Comment on lines +21 to +24
@click.option(
"--platform",
required=True,
help="The platform for the project (e.g., 'python', 'javascript-react').",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah that's true, let me refine this a bit and handle the cursor comment above. Thanks for taking a look in the meantime!

Comment thread src/sentry/runner/commands/createproject.py
@GabeVillalobos GabeVillalobos force-pushed the worktree-add-project-create-cli-command branch from a579807 to 4fff575 Compare March 30, 2026 18:21
Comment thread src/sentry/runner/commands/createproject.py
@GabeVillalobos GabeVillalobos requested a review from a team as a code owner March 30, 2026 18:25
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Additional Locations (1)
Fix in Cursor Fix in Web

@GabeVillalobos GabeVillalobos enabled auto-merge (squash) March 30, 2026 18:51
@GabeVillalobos GabeVillalobos merged commit 8da8854 into master Mar 30, 2026
65 checks passed
@GabeVillalobos GabeVillalobos deleted the worktree-add-project-create-cli-command branch March 30, 2026 19:10
@github-actions github-actions bot locked and limited conversation to collaborators Apr 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants