Skip to content

Commit df51dad

Browse files
committed
cleanup: apply AGENTS.md patterns to member project creation fallback
- 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
1 parent 2c055cd commit df51dad

5 files changed

Lines changed: 50 additions & 6 deletions

File tree

src/commands/project/create.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
createProjectWithAutoTeam,
2323
createProjectWithDsn,
2424
listTeams,
25+
MEMBER_PROJECT_CREATION_DISABLED_DETAIL,
2526
tryGetPrimaryDsn,
2627
} from "../../lib/api-client.js";
2728
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
@@ -59,7 +60,7 @@ import { slugify } from "../../lib/utils.js";
5960

6061
const log = logger.withTag("project.create");
6162

62-
/** Usage hint template — base command without positionals */
63+
/** Full usage hint shown in errors and help text. */
6364
const USAGE_HINT = "sentry project create <org>/<name> <platform>";
6465

6566
type CreateFlags = {
@@ -256,6 +257,9 @@ async function resolveDryRunTeam(
256257
if (!(error instanceof ApiError && error.status === 403) || opts.team) {
257258
throw error;
258259
}
260+
log.debug(
261+
"403 on listTeams in dry-run — previewing org-scoped fallback outcome"
262+
);
259263
return { slug: "team-<username>", source: "auto-created" };
260264
}
261265
}
@@ -289,7 +293,7 @@ async function createProjectWithAutoTeamFallback(opts: {
289293
if (expError instanceof ApiError) {
290294
if (
291295
expError.status === 403 &&
292-
expError.detail?.includes("disabled this feature")
296+
expError.detail?.includes(MEMBER_PROJECT_CREATION_DISABLED_DETAIL)
293297
) {
294298
throw new ApiError(
295299
`Failed to create project '${name}' in ${orgSlug} (HTTP 403).\n\n` +
@@ -509,7 +513,6 @@ export const createCommand = buildCommand({
509513
return yield new CommandOutput(result);
510514
}
511515

512-
// Attempt the normal team-based flow.
513516
// If either step 403s (member can't create/see teams, or lacks project:write on
514517
// the team), fall back to POST /organizations/{org}/projects/ which mirrors
515518
// what the Sentry onboarding UI uses: auto-creates a personal team for the
@@ -542,6 +545,7 @@ export const createCommand = buildCommand({
542545
if (!(error instanceof ApiError && error.status === 403) || flags.team) {
543546
throw error;
544547
}
548+
log.debug("403 on team-based flow — falling back to org-scoped endpoint");
545549
const fallback = await createProjectWithAutoTeamFallback({
546550
orgSlug,
547551
name,

src/lib/api-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export {
103103
getProjectKeys,
104104
listProjects,
105105
listProjectsPaginated,
106+
MEMBER_PROJECT_CREATION_DISABLED_DETAIL,
106107
matchesWordBoundary,
107108
type ProjectSearchResult,
108109
type ProjectWithOrg,

src/lib/api/projects.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,14 @@ type ProjectWithAutoTeam = SentryProject & {
233233
team_slug: string;
234234
};
235235

236+
/**
237+
* Substring present in the 403 detail when the org has disabled member project
238+
* creation. Callers match against this to distinguish a policy 403 from an auth
239+
* 403, so they can surface a clear "ask your admin" message instead of a generic
240+
* permission error or a re-auth prompt.
241+
*/
242+
export const MEMBER_PROJECT_CREATION_DISABLED_DETAIL = "disabled this feature";
243+
236244
/**
237245
* Create a new project via the org-scoped member-accessible endpoint.
238246
*
@@ -266,6 +274,18 @@ export async function createProjectWithAutoTeam(
266274
`/organizations/${orgSlug}/projects/`,
267275
{ method: "POST", body }
268276
);
277+
278+
// Seed project cache so subsequent commands skip redundant API lookups.
279+
// Mirrors what createProjectWithDsn does for the team-scoped endpoint.
280+
try {
281+
const orgName = resolveOrgDisplayName(orgSlug, data.organization?.name);
282+
cacheProjectsForOrg(orgSlug, orgName, [
283+
{ id: data.id, slug: data.slug, name: data.name },
284+
]);
285+
} catch {
286+
// Best-effort — don't let cache failures break project creation
287+
}
288+
269289
return data;
270290
}
271291

src/lib/init/tools/create-sentry-project.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1+
/**
2+
* Sentry project creation tool for the init wizard.
3+
*
4+
* Implements the `create-sentry-project` and `ensure-sentry-project` wizard
5+
* operations. Uses the team-scoped endpoint when the caller has team access,
6+
* falling back to POST /organizations/{org}/projects/ for org members who
7+
* lack team:write.
8+
*/
9+
110
import {
211
createProjectWithAutoTeam,
312
createProjectWithDsn,
13+
MEMBER_PROJECT_CREATION_DISABLED_DETAIL,
414
tryGetPrimaryDsn,
515
} from "../../api-client.js";
616
import { ApiError } from "../../errors.js";
@@ -26,6 +36,15 @@ type ProjectData = {
2636
/**
2737
* Resolve project creation using the team-based flow, falling back to the
2838
* org-scoped endpoint on 403 (member lacks team creation permission).
39+
*
40+
* @param opts.org - Organization slug
41+
* @param opts.name - Project display name
42+
* @param opts.platform - Platform identifier (null/undefined → omitted from request)
43+
* @param opts.explicitTeam - Team slug from `--team` flag; suppresses fallback when set
44+
* @param opts.slugHint - Slug used for auto-creating a team when org has none
45+
* @returns Resolved project identifiers and DSN
46+
* @throws When the team-scoped flow fails for any reason other than a member
47+
* permission 403, or when an explicit team was given and it 403s.
2948
*/
3049
async function resolveProjectCreation(opts: {
3150
org: string;
@@ -161,7 +180,7 @@ export async function createSentryProject(
161180
if (
162181
error instanceof ApiError &&
163182
error.status === 403 &&
164-
error.detail?.includes("disabled this feature")
183+
error.detail?.includes(MEMBER_PROJECT_CREATION_DISABLED_DETAIL)
165184
) {
166185
return {
167186
ok: false,

src/lib/resolve-team.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ async function autoCreateTeam(
255255
throw error;
256256
}
257257
// 403 means the user lacks permission to create teams (e.g., org member role).
258-
// Re-throw as ApiError so callers can fall back to the experimental
259-
// member-accessible project creation endpoint instead of showing a dead-end error.
258+
// Re-throw as ApiError so callers can fall back to the org-scoped endpoint
259+
// (POST /organizations/{org}/projects/) instead of showing a dead-end error.
260260
if (error instanceof ApiError && error.status === 403) {
261261
throw error;
262262
}

0 commit comments

Comments
 (0)