Skip to content

feat(project): add project create command#237

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

feat(project): add project create command#237
betegon wants to merge 27 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

@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 🐛

  • (api) Auto-correct ':' to '=' in --field values with a warning by BYK in #302
  • (ci) Generate JUnit XML to silence codecov-action warnings by BYK in #300
  • (nightly) Push to GHCR from artifacts dir so layer titles are bare filenames by BYK in #301

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 13, 2026

Codecov Results 📊

2111 passed | Total: 2111 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +29
Passed Tests 📈 +29
Failed Tests
Skipped Tests

All tests are passing successfully.

✅ Patch coverage is 89.43%. Project has 3640 uncovered lines.
✅ Project coverage is 76.81%. Comparing base (base) to head (head).

Files with missing lines (5)
File Patch % Lines
api-client.ts 71.07% ⚠️ 263 Missing
view.ts 88.04% ⚠️ 25 Missing
create.ts 95.70% ⚠️ 11 Missing
output.ts 90.00% ⚠️ 3 Missing
resolve-team.ts 96.10% ⚠️ 3 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    76.47%    76.81%    +0.34%
==========================================
  Files          117       119        +2
  Lines        15335     15698      +363
  Branches         0         0         —
==========================================
+ Hits         11727     12058      +331
- Misses        3608      3640       +32
- Partials         0         0         —

Generated by Codecov Action

@betegon betegon requested a review from BYK February 13, 2026 19:46
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

betegon and others added 17 commits February 26, 2026 22:25
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.
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
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.
- 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.
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.
Add NFKD normalization to handle accented characters and ligatures,
and preserve underscores to match Sentry's MIXED_SLUG_PATTERN.
@betegon betegon force-pushed the feat/project-create branch from 512093d to 1c80d67 Compare February 26, 2026 21:28
Reuse the more capable parseOrgProjectArg parser instead of the
custom parseOrgPrefixedArg, which was the only consumer. This
removes duplicated parsing logic and adds URL support for free.
…ions

Delete the old orgScopedRequest-based implementations that were left
behind after migrating to @sentry/api. These duplicates referenced
undefined identifiers (orgScopedRequest, SentryTeamSchema,
SentryProjectSchema) and would crash at runtime. Also add the
missing TEAM_ENDPOINT_REGEX constant and clean up unused imports.
Move writeKeyValue from project/create.ts to lib/formatters/output.ts
so it can be reused across commands. Adopt it in team/view.ts to
replace the missing formatTeamDetails with inline human-readable output.
The error hint referenced `sentry org view <org>` for setting a default
org, but no such mechanism exists yet. Remove it to avoid confusing
users. Tracked in #304.
}

return null;
}
Copy link

Choose a reason for hiding this comment

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

Duplicate org resolution skips environment variable check

Medium Severity

resolveOrgForTeam reimplements org resolution but skips the SENTRY_ORG environment variable check that the shared resolveOrg function in resolve-target.ts includes. When a user has SENTRY_ORG set (without SENTRY_PROJECT), sentry team view <team> fails to resolve the org while sentry project create works correctly since it uses resolveOrg.

Fix in Cursor Fix in Web

DSN detection is confusing in the project create context since you
typically don't have a DSN when creating a new project. Remove DSN
mentions from the error hint and description while keeping DSN
detection functional as a silent fallback.
The team view command imports getTeam but the function was missing from
api-client, causing a runtime TypeError. Add getTeam using the same
pattern as getProject, backed by the SDK's retrieveATeam.
Remove team view command and getTeam API function that were accidentally
included in this branch. The code imported a non-existent buildTeamUrl,
breaking typecheck. This work is preserved on feat/team-view-command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-throw AuthError in handleCreateProject404 so auth errors propagate
instead of being swallowed. Guard writeKeyValue against empty pairs to
avoid Math.max(...[]) returning -Infinity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Replace `await` with `return` on `handleCreateProject404` and
`buildOrgFailureError` calls so the `never` control flow is explicit
and subsequent code is clearly unreachable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@betegon betegon requested a review from BYK February 27, 2026 12:44
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