Skip to content

Commit cc66067

Browse files
fix(init): skip project creation when project already exists
When both org and project are explicitly provided (e.g., from resolveProjectBySlug resolving a bare slug), check if the project already exists via getProject before attempting to create it. This avoids a 409 Conflict from the create API when re-running init on an existing Sentry project. Also extract resolveOrgForInit and formatLocalOpError helpers to keep createSentryProject under the cognitive complexity limit.
1 parent 93b90fd commit cc66067

1 file changed

Lines changed: 81 additions & 22 deletions

File tree

src/lib/init/local-ops.ts

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import path from "node:path";
1111
import { isCancel, select } from "@clack/prompts";
1212
import {
1313
createProject,
14+
getProject,
1415
listOrganizations,
1516
tryGetPrimaryDsn,
1617
} from "../api-client.js";
@@ -702,6 +703,56 @@ async function resolveOrgSlug(
702703
return selected;
703704
}
704705

706+
/**
707+
* Try to fetch an existing project by org + slug. Returns a successful
708+
* LocalOpResult if the project exists, or null if it doesn't (404).
709+
* Other errors are left to propagate.
710+
*/
711+
async function tryGetExistingProject(
712+
orgSlug: string,
713+
projectSlug: string
714+
): Promise<LocalOpResult | null> {
715+
try {
716+
const project = await getProject(orgSlug, projectSlug);
717+
const dsn = await tryGetPrimaryDsn(orgSlug, project.slug);
718+
const url = buildProjectUrl(orgSlug, project.slug);
719+
return {
720+
ok: true,
721+
data: {
722+
orgSlug,
723+
projectSlug: project.slug,
724+
projectId: project.id,
725+
dsn: dsn ?? "",
726+
url,
727+
},
728+
};
729+
} catch (error) {
730+
// 404 means project doesn't exist — fall through to creation
731+
if (error instanceof ApiError && error.status === 404) {
732+
return null;
733+
}
734+
throw error;
735+
}
736+
}
737+
738+
/**
739+
* Resolve the org slug from CLI args, env, config, or interactive prompt.
740+
* Returns the org slug string, or a LocalOpResult if resolution fails or is cancelled.
741+
*/
742+
async function resolveOrgForInit(
743+
cwd: string,
744+
options: WizardOptions
745+
): Promise<string | LocalOpResult> {
746+
if (options.org) {
747+
return options.org;
748+
}
749+
const orgResult = await resolveOrgSlug(cwd, options.yes);
750+
if (typeof orgResult !== "string") {
751+
return orgResult;
752+
}
753+
return orgResult;
754+
}
755+
705756
async function createSentryProject(
706757
payload: CreateSentryProjectPayload,
707758
options: WizardOptions
@@ -732,35 +783,40 @@ async function createSentryProject(
732783
}
733784

734785
try {
735-
// 1. Resolve org — skip interactive resolution if explicitly provided via CLI arg
736-
let orgSlug: string;
737-
if (options.org) {
738-
orgSlug = options.org;
739-
} else {
740-
const orgResult = await resolveOrgSlug(payload.cwd, options.yes);
741-
if (typeof orgResult !== "string") {
742-
return orgResult;
786+
// 1. Resolve org
787+
const orgResult = await resolveOrgForInit(payload.cwd, options);
788+
if (typeof orgResult !== "string") {
789+
return orgResult;
790+
}
791+
const orgSlug = orgResult;
792+
793+
// 2. If both org and project were provided, check if the project already exists.
794+
// This avoids a 409 Conflict from the create API when re-running init on an
795+
// existing Sentry project (e.g., bare slug resolved via resolveProjectBySlug).
796+
if (options.org && options.project) {
797+
const existing = await tryGetExistingProject(orgSlug, slug);
798+
if (existing) {
799+
return existing;
743800
}
744-
orgSlug = orgResult;
745801
}
746802

747-
// 2. Resolve or create team
803+
// 3. Resolve or create team
748804
const team = await resolveOrCreateTeam(orgSlug, {
749805
team: options.team,
750806
autoCreateSlug: slug,
751807
usageHint: "sentry init",
752808
});
753809

754-
// 3. Create project
810+
// 4. Create project
755811
const project = await createProject(orgSlug, team.slug, {
756812
name,
757813
platform,
758814
});
759815

760-
// 4. Get DSN (best-effort)
816+
// 5. Get DSN (best-effort)
761817
const dsn = await tryGetPrimaryDsn(orgSlug, project.slug);
762818

763-
// 5. Build URL
819+
// 6. Build URL
764820
const url = buildProjectUrl(orgSlug, project.slug);
765821

766822
return {
@@ -774,14 +830,17 @@ async function createSentryProject(
774830
},
775831
};
776832
} catch (error) {
777-
let message: string;
778-
if (error instanceof ApiError) {
779-
message = error.format();
780-
} else if (error instanceof Error) {
781-
message = error.message;
782-
} else {
783-
message = String(error);
784-
}
785-
return { ok: false, error: message };
833+
return { ok: false, error: formatLocalOpError(error) };
834+
}
835+
}
836+
837+
/** Format an error from a local-op into a user-facing message string. */
838+
function formatLocalOpError(error: unknown): string {
839+
if (error instanceof ApiError) {
840+
return error.format();
841+
}
842+
if (error instanceof Error) {
843+
return error.message;
786844
}
845+
return String(error);
787846
}

0 commit comments

Comments
 (0)