Skip to content

Comments

feat(project): add project create command#237

Open
betegon wants to merge 21 commits intomainfrom
feat/project-create
Open

feat(project): add project create command#237
betegon wants to merge 21 commits intomainfrom
feat/project-create

Conversation

@betegon
Copy link
Member

@betegon betegon commented Feb 12, 2026

Summary

Adds sentry project create <name> <platform> — the first write command in the CLI. Follows gh repo create conventions with org/name syntax, auto-detection of org and team, and actionable errors at every step.

Changes

  • New SentryTeam Zod schema and type
  • listTeams() and createProject() API functions with region-aware routing (TEAM_ENDPOINT_REGEX)
  • project create command with two required positionals, --team/-t flag, and --json output
  • Team auto-selection when org has exactly one team; lists available teams otherwise
  • Org fallback: on 404 from bad auto-detected org, shows user's actual organizations
  • DSN fetched after creation (best-effort) so the user can start sending events immediately
  • 19 tests covering happy path, missing args, team resolution, conflict handling, and JSON output

Test Plan

  • bun test test/commands/project/create.test.ts — 19 pass, 0 fail
  • bun run typecheck — clean
  • bun run lint — clean
  • Manually tested against live API: create with auto-team, explicit team, bad team, 409 conflict, missing args, JSON mode

Add team schema/type and two new API functions needed for project
creation. Also adds TEAM_ENDPOINT_REGEX so /teams/{org}/... endpoints
route to the correct region.
Adds `sentry project create <name> <platform> [--team] [--json]`.

Supports org/name syntax (like gh repo create owner/repo), auto-detects
org from config/DSN, and auto-selects team when the org has exactly one.
Fetches the DSN after creation so users can start sending events
immediately. All error paths are actionable — wrong org lists your orgs,
wrong team lists available teams, 409 links to the existing project.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (project) Add project create command by betegon in #237

Bug Fixes 🐛

  • (help) Document target patterns and trailing-slash significance by BYK in #272
  • (npm) Add Node.js >= 22 version guard to npm bundle by BYK in #269

Internal Changes 🔧

  • (org) Use shared list-command constants in org list by BYK in #273

🤖 This preview updates automatically when you update the PR.

github-actions bot and others added 4 commits February 12, 2026 20:14
When the API returns 400 for an invalid platform string, show the same
helpful platform list instead of a raw JSON error body.
Replace the confusing 'Or: - Available platforms:' pattern with a
cleaner 'Usage: ... Available platforms:' layout. Applies to both
missing platform and invalid platform errors.
- tryGetPrimaryDsn() → api-client.ts (was duplicated in view + create)
- resolveTeam() → resolve-team.ts (reusable for future team-dependent commands)
- parseOrgPrefixedArg() → arg-parsing.ts (reusable org/name parsing)
- writeKeyValue() for aligned key-value output in create.ts
- project/view.ts now uses shared tryGetPrimaryDsn instead of local copy
@github-actions
Copy link
Contributor

github-actions bot commented Feb 13, 2026

Codecov Results 📊

✅ Patch coverage is 93.35%. Project has 3606 uncovered lines.
✅ Project coverage is 75.07%. Comparing base (base) to head (head).

Files with missing lines (71)
File Patch % Lines
human.ts 58.39% ⚠️ 394 Missing
resolve-target.ts 37.26% ⚠️ 325 Missing
api-client.ts 69.99% ⚠️ 253 Missing
oauth.ts 30.94% ⚠️ 183 Missing
list.ts 29.55% ⚠️ 155 Missing
plan.ts 19.37% ⚠️ 154 Missing
list.ts 72.41% ⚠️ 120 Missing
resolver.ts 3.23% ⚠️ 120 Missing
help.ts 19.85% ⚠️ 109 Missing
upgrade.ts 61.37% ⚠️ 107 Missing
view.ts 41.90% ⚠️ 104 Missing
interactive-login.ts 9.17% ⚠️ 99 Missing
errors.ts 5.94% ⚠️ 95 Missing
view.ts 25.81% ⚠️ 92 Missing
view.ts 41.50% ⚠️ 86 Missing
clipboard.ts 4.49% ⚠️ 85 Missing
status.ts 24.07% ⚠️ 82 Missing
migration.ts 47.44% ⚠️ 82 Missing
list.ts 27.18% ⚠️ 75 Missing
browser.ts 4.11% ⚠️ 70 Missing
list.ts 86.08% ⚠️ 59 Missing
span-tree.ts 5.00% ⚠️ 57 Missing
explain.ts 33.33% ⚠️ 56 Missing
api.ts 89.80% ⚠️ 47 Missing
upgrade.ts 66.91% ⚠️ 46 Missing
seer.ts 75.54% ⚠️ 45 Missing
schema.ts 91.51% ⚠️ 40 Missing
refresh.ts 40.63% ⚠️ 38 Missing
seer.ts 79.87% ⚠️ 30 Missing
preload.ts 53.23% ⚠️ 29 Missing
telemetry.ts 93.01% ⚠️ 27 Missing
utils.ts 88.94% ⚠️ 25 Missing
view.ts 61.54% ⚠️ 25 Missing
view.ts 87.86% ⚠️ 25 Missing
fix.ts 89.43% ⚠️ 24 Missing
org-list.ts 93.81% ⚠️ 21 Missing
detector.ts 90.10% ⚠️ 20 Missing
arg-parsing.ts 91.51% ⚠️ 18 Missing
binary.ts 88.67% ⚠️ 17 Missing
help.ts 57.14% ⚠️ 15 Missing
sentry-client.ts 92.17% ⚠️ 13 Missing
dsn-cache.ts 94.62% ⚠️ 12 Missing
code-scanner.ts 96.25% ⚠️ 12 Missing
logout.ts 56.00% ⚠️ 11 Missing
token.ts 52.17% ⚠️ 11 Missing
qrcode.ts 33.33% ⚠️ 10 Missing
fs-utils.ts 57.14% ⚠️ 9 Missing
view.ts 94.89% ⚠️ 7 Missing
project-root.ts 97.73% ⚠️ 7 Missing
version-check.ts 92.47% ⚠️ 7 Missing
feedback.ts 84.21% ⚠️ 6 Missing
create.ts 97.57% ⚠️ 6 Missing
auth.ts 95.56% ⚠️ 6 Missing
shell.ts 96.23% ⚠️ 6 Missing
app.ts 94.05% ⚠️ 5 Missing
setup.ts 97.84% ⚠️ 4 Missing
resolve-team.ts 96.10% ⚠️ 3 Missing
project-aliases.ts 97.40% ⚠️ 2 Missing
project-root-cache.ts 96.92% ⚠️ 2 Missing
output.ts 89.47% ⚠️ 2 Missing
login.ts 98.72% ⚠️ 1 Missing
list.ts 99.01% ⚠️ 1 Missing
alias.ts 99.42% ⚠️ 1 Missing
completions.ts 99.37% ⚠️ 1 Missing
index.ts 98.99% ⚠️ 1 Missing
env-file.ts 99.19% ⚠️ 1 Missing
parser.ts 98.63% ⚠️ 1 Missing
colors.ts 98.21% ⚠️ 1 Missing
trace.ts 99.16% ⚠️ 1 Missing
region.ts 97.30% ⚠️ 1 Missing
helpers.ts 94.74% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    74.59%    75.07%    +0.48%
==========================================
  Files          115       117        +2
  Lines        14107     14465      +358
  Branches         0         0         —
==========================================
+ Hits         10523     10859      +336
- Misses        3584      3606       +22
- Partials         0         0         —

Generated by Codecov Action

# Conflicts:
#	src/lib/api-client.ts
The /teams/{org}/{team}/projects/ endpoint returns 404 for both a bad
org and a bad team. Previously we always blamed the team, which was
misleading when --team was explicit and the org was auto-detected wrong.

Now on 404 we call listTeams(orgSlug) to check:
- If it succeeds → team is wrong, show available teams
- If it fails → org is wrong, show user's actual organizations

Only adds an API call on the error path, never on the happy path.
The view command hint on 409 used the raw name ('My Cool App') instead
of the expected slug ('my-cool-app'), pointing to a non-existent target.
handleCreateProject404 was treating any listTeams failure as proof that
the org doesn't exist. Now it checks the status code: only 404 triggers
'Organization not found'. Other failures (403, 5xx, network) get a
generic message that doesn't misdiagnose the root cause.
Same class of bug as the previous fix in handleCreateProject404:
resolveTeam was routing all ApiErrors from listTeams into the 'org not
found' path. Now only 404 triggers that diagnosis. Other failures
(403, 5xx) get a generic message that doesn't misdiagnose the cause.
@betegon betegon requested a review from BYK February 13, 2026 19:46
# Conflicts:
#	plugins/sentry-cli/skills/sentry-cli/SKILL.md
#	src/lib/api-client.ts
#	src/lib/arg-parsing.ts
#	src/types/sentry.ts
- Extract shared fetchOrgListHint() in resolve-team.ts to deduplicate
  org-list fetching logic (used by both resolve-team and create 404 handler)
- Use Writer type instead of inline { write } in writeKeyValue
- Simplify Awaited<ReturnType<typeof listTeams>> to SentryTeam[]
- Add fragility comment to isPlatformError (relies on API message wording)
- Fix test import to use barrel (types/index.js)
When a project slug is already taken, Sentry silently appends a random
suffix (e.g., 'test1' becomes 'test1-0g'). This was confusing because
the user had no indication why the slug differed from the name.

Now shows: Note: Slug 'test1-0g' was assigned because 'test1' is already taken.
Copy link
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

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

Love that you both try to infer team and org automatically while supporting org/proj --team <team> syntax too.

See my comments and decide whether you wanna merge as it is or not. My 2 major concerns are the refactor (I may merge that before you see this) and the error when we find more than 1 team.

* parseOrgPrefixedArg("acme/my-app", "Project name", "sentry project create <org>/<name>")
* // { org: "acme", name: "my-app" }
*/
export function parseOrgPrefixedArg(
Copy link
Member

Choose a reason for hiding this comment

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

I'm doing a massive refactor which may make this part obsolete.

Copy link
Member Author

Choose a reason for hiding this comment

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

finally looked through the current infra after merging main, parseOrgProjectArg and dispatchOrgScopedList are for list/query commands. parseOrgPrefixedArg parses a new resource name, not an existing slug, and resolveTeam handles team auto-selection for write operations.

So i don't think they have an equivalent in your changes, maybe we could consolidate later with more create commands?

* @returns Team slug to use
* @throws {ContextError} When team cannot be resolved
*/
export async function resolveTeam(
Copy link
Member

Choose a reason for hiding this comment

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

Same as above

Previously, project create errored whenever an org had 2+ teams,
requiring --team in every non-trivial org. Now filters teams by
isMember and auto-selects when the user belongs to exactly one team.

When multiple member teams exist, only those are shown in the error
(not all org teams). Falls back to the full list when isMember data
is unavailable (self-hosted, old API).
…ists

handleCreateProject404 now checks if the teamSlug is actually present
in the returned teams list before claiming it's not found. When the team
exists (e.g., auto-selected by resolveTeam), the error correctly reports
a permission issue instead of the contradictory message.
Copy link

@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 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

};

/** Common Sentry platform strings, shown when platform arg is missing or invalid */
const PLATFORMS = [
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

my thinking here was to show just a small list of the most used/popular ones If we show the 140, that would be way harder to read and fills all the terminal screen

Comment on lines +61 to +67
/**
* Convert a project name to its expected Sentry slug.
* Sentry slugs are lowercase, with non-alphanumeric runs replaced by hyphens.
*
* @example slugify("My Cool App") // "my-cool-app"
* @example slugify("my-app") // "my-app"
*/
Copy link
Member

Choose a reason for hiding this comment

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

Do we have this logic coded somewhere in the Sentry app? We should and if we do, this comment should refer to that specific location.

"Usage:\n" +
` sentry project create ${nameArg} <platform>\n\n` +
`Available platforms:\n\n${list}\n\n` +
"Full list: https://docs.sentry.io/platforms/"
Copy link
Member

Choose a reason for hiding this comment

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

See my earlier comment: we should list the ones from the release regists.

Comment on lines +151 to +154
throw new CliError(
`No teams found in ${orgSlug}.\n\n` +
"Create a team first, then try again."
);
Copy link
Member

Choose a reason for hiding this comment

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

Can we offer creating a new, default team on the fly? Or actually "just do it"?

* Write key-value pairs with aligned columns.
* Used for human-readable output after resource creation.
*/
function writeKeyValue(
Copy link
Member

Choose a reason for hiding this comment

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

This should be a shared utility across all commands

fullDescription:
"Create a new Sentry project in an organization.\n\n" +
"The name supports org/name syntax to specify the organization explicitly.\n" +
"If omitted, the org is auto-detected from config defaults or DSN.\n\n" +
Copy link
Member

Choose a reason for hiding this comment

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

It's a bit confusing to me to detect anything form the DSN under this context as if we are creating a new project, that implies we don't typically have a DSN around?

if (!resolved) {
throw new ContextError("Organization", USAGE_HINT, [
`Include org in name: ${USAGE_HINT}`,
"Set a default: sentry org view <org>",
Copy link
Member

Choose a reason for hiding this comment

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

This is not how we set a default org. In fact we don't have the notion of a default org but I like it. Good follow up PR.

const regionUrl = await resolveOrgRegion(orgSlug);
const { data } = await apiRequestToRegion<SentryProject>(
regionUrl,
`/teams/${orgSlug}/${teamSlug}/projects/`,
Copy link
Member

Choose a reason for hiding this comment

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

Is the reason to not use @sentry/api things because they are not region-aware? If that's the case I think we should add the region-aware version to that package @MathurAditya724

}

/** Parsed result from an `org/name` positional argument */
export type ParsedOrgPrefixed = {
Copy link
Member

Choose a reason for hiding this comment

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

I'm 100% sure we have something better than this introduced in https://github.com/getsentry/cli/pull/262/changes (or already existing)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants