Skip to content

feat(project): fall back to org-scoped endpoint for member project creation#1030

Merged
betegon merged 17 commits into
mainfrom
ref/org-scoped-project-creation
May 29, 2026
Merged

feat(project): fall back to org-scoped endpoint for member project creation#1030
betegon merged 17 commits into
mainfrom
ref/org-scoped-project-creation

Conversation

@betegon
Copy link
Copy Markdown
Member

@betegon betegon commented May 28, 2026

Problem

sentry project create and sentry init both failed with 403 for org members. The team-scoped endpoint (POST /teams/{org}/{team}/projects/) requires project:write, which the member role doesn't have. Trying to auto-create a team first also 403s since that requires team:write.

Solution

Fall back to POST /organizations/{org}/projects/ on 403. This endpoint:

  • only requires project:read (members have this)
  • auto-creates a personal team (team-{username}) for the caller
  • is what the Sentry onboarding UI uses for members

The fallback is skipped when --team is passed explicitly — in that case the 403 is meaningful feedback, not a permissions gap.

Before:

$ sentry project create chisme/my-project javascript-nextjs
Error: Failed to create project 'my-project' in chisme (HTTP 403).
  You do not have permission to perform this action.

After:

$ sentry project create chisme/my-project javascript-nextjs
Created project 'my-project' in chisme
  Team: team-miguelbetegon (auto-created)
  DSN: https://...

Changes

  • src/lib/api/projects.tscreateProjectWithAutoTeam() + ProjectWithAutoTeam type, calls POST /organizations/{org}/projects/
  • src/lib/resolve-team.ts — re-throw ApiError 403 from autoCreateTeam (instead of wrapping in CliError) so callers can detect and fall back
  • src/commands/project/create.tscreateProjectWithAutoTeamFallback() helper; main func calls it on 403
  • src/lib/init/tools/create-sentry-project.tsresolveProjectCreation() helper with the same fallback for sentry init

Both helpers are extracted to keep parent functions under biome's complexity limit (max 15).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-1030/

Built to branch gh-pages at 2026-05-29 11:48 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

…eation

sentry project create and sentry init both previously failed with 403
for org members who can't create teams — the team-scoped endpoint
(POST /teams/{org}/{team}/projects/) requires project:write, which members
don't have.

Add a fallback to POST /organizations/{org}/projects/, which only requires
project:read and auto-creates a personal team (team-{username}) for the
caller. This mirrors what the Sentry onboarding UI does.

Changes:
- projects.ts: add createProjectWithAutoTeam (+ ProjectWithAutoTeam type)
  that calls POST /organizations/{org}/projects/
- resolve-team.ts: re-throw ApiError 403 from autoCreateTeam instead of
  wrapping in CliError, so callers can detect and fall back
- create.ts: on 403 from team resolution or project creation, call
  createProjectWithAutoTeamFallback; skip fallback when --team is explicit
- create-sentry-project.ts: same fallback via resolveProjectCreation helper

Both helpers are extracted to keep the calling functions under the
complexity limit (biome noExcessiveCognitiveComplexity, max 15).
@betegon betegon force-pushed the ref/org-scoped-project-creation branch from ff4627a to 0e60e1d Compare May 28, 2026 18:23
Comment thread src/commands/project/create.ts
Comment thread src/lib/init/tools/create-sentry-project.ts
@betegon betegon marked this pull request as ready for review May 28, 2026 19:53
…ion fallback

- create-sentry-project.ts: coerce platform null → undefined to match
  CreateProjectBody type (TS2322)
- create.test.ts: add createProjectWithAutoTeamSpy; split the 403 test
  into three cases: non-403 errors surface directly, 403 tries fallback,
  403 with org policy disabled surfaces policy error
Comment thread src/lib/init/tools/create-sentry-project.ts Outdated
The fallback to POST /organizations/{org}/projects/ was returning
dsn: "" — the comment claimed DSN was fetched separately downstream,
but no such secondary fetch exists in the init flow. Mastra would
receive an empty DSN and generate SDK config without a valid key.

Mirror create.ts which correctly calls tryGetPrimaryDsn after the
auto-team project is created.
Comment thread src/commands/project/create.ts
Comment thread src/commands/project/create.ts
Two gaps in the org-scoped fallback path:

1. listTeams 403 → ResolutionError: when listTeams itself 403s (member
   lacks team:read), resolve-team.ts was wrapping it in ResolutionError
   instead of re-throwing the ApiError. The catch condition in create.ts
   checks instanceof ApiError, so the fallback never triggered. Fix:
   extract handleListTeamsError helper (also reduces resolveOrCreateTeam
   complexity back under the biome limit) and re-throw 403 as-is.

2. Fallback 409 → raw ApiError: if createProjectWithAutoTeam 409s
   (project already exists from a prior creation via the org endpoint),
   the raw ApiError propagated instead of the friendly 'project already
   exists' CliError that the primary path uses. Fix: handle 409 in
   createProjectWithAutoTeamFallback the same way createProjectWithErrors
   does.
Comment thread src/lib/api-client.ts Outdated
Comment thread src/lib/init/tools/create-sentry-project.ts
@betegon
Copy link
Copy Markdown
Member Author

betegon commented May 29, 2026

Self-review — 4 findings

🔴 test/lib/init/tools/create-sentry-project.test.ts:265 — test broken by new fallback logic

The test mocks createProjectWithDsnSpy to throw ApiError(403, "disabled this feature"). With the new resolveProjectCreation, that 403 now triggers the createProjectWithAutoTeam fallback — but createProjectWithAutoTeam is never mocked in this test file. The auto-mock preserves the real implementation, which makes a live HTTP call. The intended assertion (result.error contains "disabled for members") is never reached.

Fix: add createProjectWithAutoTeamSpy to this test file and mock it to also throw ApiError(403, "Your organization has disabled this feature for members.") — same pattern as create.test.ts's beforeEach.


🔴 test/commands/project/create.test.ts:673 — test will fail after handleListTeamsError change

handleListTeamsError now re-throws ApiError(403) raw instead of wrapping in ResolutionError. The raw 403 hits the outer catch in create.ts, triggers the auto-team fallback, and the test ends up with ApiError about "disabled project creation" — not the CliError containing "could not be accessed (403)" it expects.

Fix: update the test to reflect the new behavior: assert the fallback is triggered and the fallback result surfaces (success or policy-error), not the old ResolutionError.


🟡 src/lib/api/projects.ts:259createProjectWithAutoTeam skips cache seeding

createProjectWithDsn seeds cacheProjectsForOrg and setCachedProjectByDsnKey after creation. createProjectWithAutoTeam does neither. After a fallback-path creation, the project is invisible to shell completions and DSN-based lookup until the cache refills.

Fix: add the same cache-seeding block that createProjectWithDsn has.


🟡 create.ts:259 + create-sentry-project.ts:164"disabled this feature" string duplicated

Two independent error.detail?.includes("disabled this feature") checks with no shared constant. If the API error message changes, both fail silently.

Fix: extract to a shared named constant.

handleListTeamsError now re-throws ApiError(403) instead of wrapping
in ResolutionError, so the outer catch triggers the org-scoped fallback.
The test was asserting the old CliError/ResolutionError path which no
longer fires. Updated to assert the new path: fallback attempted,
policy-error surfaces when the fallback is also blocked.
Comment thread src/commands/project/create.ts
--dry-run called resolveOrCreateTeam without catching the ApiError(403)
that handleListTeamsError now re-throws when listTeams 403s. A member
who would succeed with a normal run (fallback to org-scoped endpoint)
got a raw 403 error on --dry-run instead.

Extract resolveDryRunTeam helper that mirrors the non-dry-run catch:
on 403 and no explicit --team, return a placeholder team slug that
previews the auto-created team outcome without hitting the API.
Comment thread src/lib/init/tools/create-sentry-project.ts Outdated
Comment thread src/lib/resolve-team.ts
handleListTeamsError re-throws ApiError(403) so the project create
command can detect it and use the org-scoped fallback. But preflight.ts
resolveTeam converted every non-WizardCancelledError into WizardError,
aborting the wizard before create-sentry-project.ts:resolveProjectCreation
(which has the createProjectWithAutoTeam fallback) ever ran.

Fix: catch ApiError(403) in resolveTeam and return undefined — same as
the existing 'deferred' path for empty orgs. The wizard then continues
to createSentryProject, context.team is undefined, resolveProjectCreation
calls resolveOrCreateTeam which 403s again, and the catch there routes
to createProjectWithAutoTeam. The fallback is now reachable for members
in both sentry project create and sentry init.
Comment thread src/lib/api/projects.ts
Comment thread src/lib/init/tools/create-sentry-project.ts
- projects.ts: extract MEMBER_PROJECT_CREATION_DISABLED_DETAIL constant
  (shared across two call sites; prevents silent drift if server message
  changes) and add cache seeding to createProjectWithAutoTeam (mirrors
  createProjectWithDsn)
- api-client.ts: re-export the new constant
- create.ts: import and use the constant; add log.debug() in both silent
  403-fallback catch blocks (AGENTS.md: no silent catch without logging);
  remove narrating comment ('Attempt the normal team-based flow.'); fix
  USAGE_HINT JSDoc ('without positionals' was wrong, value includes them)
- create-sentry-project.ts: add file-level module JSDoc (AGENTS.md: lib
  files must have one); use constant instead of string literal; add
  @param tags to resolveProjectCreation
- resolve-team.ts: replace 'experimental' with 'org-scoped endpoint' in
  autoCreateTeam comment
Comment thread src/lib/init/tools/create-sentry-project.ts
Comment thread src/commands/project/create.ts
When createProjectWithDsn or createProjectWithAutoTeam returns 409
(project already exists), the error propagated to formatToolError which
produced raw API text. Init tools never throw — they return { ok: false }.

Add a 409 branch in createSentryProject's outer catch alongside the
existing 403-policy handler. This covers both the team-scoped and
org-scoped fallback paths since both propagate to the same catch block.
Comment thread src/lib/api-client.ts
Comment thread src/lib/init/tools/create-sentry-project.ts
1. DSN cache seeding (projects.ts): createProjectWithAutoTeam now calls
   tryGetPrimaryDsn and seeds setCachedProjectByDsnKey, mirroring the
   exact block from createProjectWithDsn (lines 209-225). Previously
   only cacheProjectsForOrg was seeded, leaving DSN-based resolution broken.

2. Policy-403 short-circuit (create-sentry-project.ts): resolveProjectCreation
   now checks MEMBER_PROJECT_CREATION_DISABLED_DETAIL before falling back to
   createProjectWithAutoTeam. Org-policy 403 is re-thrown immediately so the
   outer catch surfaces the friendly message without a wasted API round-trip.
   Follows the same check pattern as createProjectWithAutoTeamFallback in create.ts.

3. Dry-run team validation (create-sentry-project.ts): extract validateTeamForDryRun
   helper (mirrors preflight.ts:resolveTeam pattern) and call it before the
   context.dryRun early return. Previously our refactor moved the early return
   before resolveProjectCreation, skipping team validation in dry-run entirely.
   Uses deferAutoCreateOnEmptyOrg:true (same as preflight.ts line 317).
Comment thread src/lib/init/tools/create-sentry-project.ts Outdated
Comment thread src/lib/api/projects.ts Outdated
Comment thread src/lib/init/tools/create-sentry-project.ts Outdated
Comment thread src/lib/api/projects.ts
1. validateTeamForDryRun now runs only during dry-run (issue: it was
   called unconditionally before the dryRun check, causing listTeams to
   fire on every real run before resolveProjectCreation also called it).

2. Add isExplicitTeam to ResolvedInitContext (types.ts + preflight.ts).
   context.team was set by BOTH --team flag AND auto-selection, so
   resolveProjectCreation was suppressing the org-scoped fallback for
   auto-selected teams too. isExplicitTeam=Boolean(initial.team) in
   buildResolvedInitContext distinguishes flag-driven from auto-selected.
   Pass context.isExplicitTeam ? context.team : undefined as explicitTeam.

3. createProjectWithAutoTeam now returns CreatedAutoTeamProjectDetails
   ({ project, dsn, url, team_slug }) — parallel to CreatedProjectDetails
   from createProjectWithDsn. The DSN was already fetched internally for
   cache seeding but discarded, forcing both callers to call tryGetPrimaryDsn
   again. Callers now use result.dsn/url directly; unused tryGetPrimaryDsn
   and buildProjectUrl imports removed from create.ts and
   create-sentry-project.ts.
Comment thread src/commands/project/create.ts
Comment thread test/commands/project/create.test.ts
…e command

- create.test.ts: createProjectWithAutoTeamSpy mock now matches
  CreatedAutoTeamProjectDetails shape ({ project, dsn, url, team_slug })
  instead of spreading sampleProject flat. Return type changed in 9346deb.

- create.ts: add MEMBER_PROJECT_CREATION_DISABLED_DETAIL check in the
  outer catch before calling createProjectWithAutoTeamFallback — mirrors
  the same guard in resolveProjectCreation. Avoids a wasted API round-trip
  when the org has disabled member project creation.
Comment thread src/lib/init/tools/create-sentry-project.ts Outdated
…Creation

The previous explicitTeam parameter served two conflated purposes:
1. Which team slug to use for project creation
2. Whether to suppress the 403 fallback

isExplicitTeam ? context.team : undefined discarded pre-selected teams
(auto-selected by preflight), causing resolveOrCreateTeam to be called
again — breaking multi-team orgs where the user already chose a team.

Split into two parameters:
- team: the team to use (context.team, whether explicit or auto-selected)
- suppressFallback: only suppress when --team was passed (isExplicitTeam)

This preserves preflight team selection while allowing the org-scoped
fallback for members who lack project:write on their auto-selected team.

Also update the test to not assert dryRun:false on resolveOrCreateTeam —
the dry-run path exits before resolveProjectCreation is called.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

Codecov Results 📊

✅ Patch coverage is 81.71%. Project has 4297 uncovered lines.
❌ Project coverage is 82%. Comparing base (base) to head (head).

Files with missing lines (5)
File Patch % Lines
src/commands/project/create.ts 72.73% ⚠️ 9 Missing and 3 partials
src/lib/resolve-team.ts 60.00% ⚠️ 4 Missing and 2 partials
src/lib/init/tools/create-sentry-project.ts 90.00% ⚠️ 2 Missing and 1 partials
src/lib/api/projects.ts 100.00% ⚠️ 1 partials
src/lib/init/preflight.ts 100.00% ⚠️ 1 partials
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    82.01%    82.00%    -0.01%
==========================================
  Files          329       329         —
  Lines        23806     23870       +64
  Branches     15543     15595       +52
==========================================
+ Hits         19522     19573       +51
- Misses        4284      4297       +13
- Partials      1643      1648        +5

Generated by Codecov Action

Adds 10 new tests across three files to bring coverage from ~57% to
target 80% for the new fallback paths:

create-sentry-project.test.ts:
- fallback to createProjectWithAutoTeam on 403 from team-based creation
- suppressFallback:true (isExplicitTeam) prevents fallback for --team flag
- policy 403 (disabled this feature) skips fallback without round-trip
- 409 from fallback surfaces friendly 'already exists' error

preflight.test.ts:
- isExplicitTeam:true when --team flag provided
- isExplicitTeam:false when no flag (auto-selected team)
- resolveTeam 403 swallowed, context.team:undefined (enables fallback downstream)

api-client.coverage.test.ts:
- createProjectWithAutoTeam happy path (POST url, DSN, team_slug in result)
- 403 from disabled org policy propagated
- DSN:null when key fetch returns empty list
Comment thread src/lib/api-client.ts Outdated
- projects.ts + api-client.ts: type was exported but never consumed
  externally — only used as the return type of createProjectWithAutoTeam
  within projects.ts. Make it module-private (same fix as ProjectWithAutoTeam).

- create-sentry-project.test.ts: move createProjectWithAutoTeamSpy to
  beforeEach/afterEach alongside the other spies; extract sampleAutoTeamResult
  constant so per-test mocks don't repeat the object; remove per-test
  mockRestore() calls and section comment.
Comment thread src/lib/api/projects.ts Outdated
…ching

Both createProjectWithDsn and createProjectWithAutoTeam had an identical
~25-line block seeding cacheProjectsForOrg and setCachedProjectByDsnKey.
Extract into a private seedProjectCaches(orgSlug, project, dsn) helper
so future cache logic changes apply consistently to both paths.
Comment thread src/lib/api/projects.ts
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.

Reviewed by Cursor Bugbot for commit f4d0188. Configure here.

Comment thread src/commands/project/create.ts
@betegon betegon merged commit 429d85c into main May 29, 2026
29 checks passed
@betegon betegon deleted the ref/org-scoped-project-creation branch May 29, 2026 12:10
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.

1 participant