Skip to content

Commit bf80e53

Browse files
betegonclaude
andcommitted
refactor(init): reuse resolveOrg for offline-first org detection
resolveOrgSlug was reimplementing org detection (DSN scanning + numeric ID resolution) that resolveOrg in resolve-target.ts already handles, including env vars and config defaults that the init version missed. Now calls resolveOrg() first (flags, env vars, defaults, DSN), and only falls through to listOrganizations + interactive select when it returns null. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 04a1efd commit bf80e53

File tree

2 files changed

+24
-58
lines changed

2 files changed

+24
-58
lines changed

src/lib/init/local-ops.ts

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
tryGetPrimaryDsn,
1717
} from "../api-client.js";
1818
import { ApiError } from "../errors.js";
19+
import { resolveOrg } from "../resolve-target.js";
1920
import { resolveOrCreateTeam } from "../resolve-team.js";
2021
import { buildProjectUrl } from "../sentry-urls.js";
2122
import { slugify } from "../utils.js";
@@ -25,7 +26,6 @@ import {
2526
MAX_FILE_BYTES,
2627
MAX_OUTPUT_BYTES,
2728
} from "./constants.js";
28-
import { resolveOrgPrefetched } from "./prefetch.js";
2929
import type {
3030
ApplyPatchsetPayload,
3131
CreateSentryProjectPayload,
@@ -40,9 +40,6 @@ import type {
4040
WizardOptions,
4141
} from "./types.js";
4242

43-
/** Matches a bare numeric org ID extracted from a DSN (e.g. "4507492088676352"). */
44-
const NUMERIC_ORG_ID_RE = /^\d+$/;
45-
4643
/** Whitespace characters used for JSON indentation. */
4744
const Indenter = {
4845
SPACE: " ",
@@ -710,38 +707,28 @@ async function applyPatchset(
710707
}
711708

712709
/**
713-
* Resolve the org slug from local config, env vars, or by listing the user's
714-
* organizations from the API as a fallback.
710+
* Resolve the org slug using the shared offline-first resolver, falling back
711+
* to interactive selection when multiple orgs are available.
715712
*
716-
* DSN scanning uses the prefetch-aware helper from `./prefetch.ts` — if
717-
* {@link warmOrgDetection} was called earlier (by `init.ts`), the result is
718-
* already cached and returns near-instantly.
713+
* Resolution priority (via {@link resolveOrg}):
714+
* 1. CLI `--org` flag
715+
* 2. `SENTRY_ORG` / `SENTRY_PROJECT` env vars
716+
* 3. Config defaults (SQLite)
717+
* 4. DSN auto-detection (with numeric ID normalization)
719718
*
720-
* `listOrganizations()` uses SQLite caching for near-instant warm lookups
721-
* (populated after `sentry login` or the first API call), so it does not
722-
* need background prefetching.
719+
* If none of the above resolve, lists the user's organizations (SQLite-cached
720+
* after `sentry login`) and prompts for selection.
723721
*
724722
* @returns The org slug on success, or a {@link LocalOpResult} error to return early.
725723
*/
726724
export async function resolveOrgSlug(
727725
cwd: string,
728726
yes: boolean
729727
): Promise<string | LocalOpResult> {
730-
const resolved = await resolveOrgPrefetched(cwd);
728+
// Reuse the shared offline-first resolver (flags, env vars, defaults, DSN)
729+
const resolved = await resolveOrg({ cwd });
731730
if (resolved) {
732-
// If the detected org is a raw numeric ID (extracted from a DSN), try to
733-
// resolve it to a real slug. Numeric IDs can fail for write operations like
734-
// project/team creation, and may belong to a different Sentry account.
735-
if (NUMERIC_ORG_ID_RE.test(resolved.org)) {
736-
const { getOrgByNumericId } = await import("../db/regions.js");
737-
const match = getOrgByNumericId(resolved.org);
738-
if (match) {
739-
return match.slug;
740-
}
741-
// Cache miss — fall through to listOrganizations() for proper selection
742-
} else {
743-
return resolved.org;
744-
}
731+
return resolved.org;
745732
}
746733

747734
// Fallback: list user's organizations (SQLite-cached after login/first call)

test/lib/init/local-ops.create-sentry-project.test.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ describe("create-sentry-project", () => {
194194

195195
describe("resolveOrgSlug (called directly)", () => {
196196
test("single org fallback when resolveOrg returns null", async () => {
197-
resolveOrgPrefetchedSpy.mockResolvedValue(null);
197+
resolveOrgSpy.mockResolvedValue(null);
198198
listOrgsSpy.mockResolvedValue([
199199
{ id: "1", slug: "solo-org", name: "Solo Org" },
200200
]);
@@ -206,7 +206,7 @@ describe("create-sentry-project", () => {
206206
});
207207

208208
test("no orgs (not authenticated) returns error result", async () => {
209-
resolveOrgPrefetchedSpy.mockResolvedValue(null);
209+
resolveOrgSpy.mockResolvedValue(null);
210210
listOrgsSpy.mockResolvedValue([]);
211211

212212
const result = await resolveOrgSlug("/tmp/test", false);
@@ -218,7 +218,7 @@ describe("create-sentry-project", () => {
218218
});
219219

220220
test("multiple orgs + yes flag returns error with slug list", async () => {
221-
resolveOrgPrefetchedSpy.mockResolvedValue(null);
221+
resolveOrgSpy.mockResolvedValue(null);
222222
listOrgsSpy.mockResolvedValue([
223223
{ id: "1", slug: "org-a", name: "Org A" },
224224
{ id: "2", slug: "org-b", name: "Org B" },
@@ -235,7 +235,7 @@ describe("create-sentry-project", () => {
235235
});
236236

237237
test("multiple orgs + interactive select picks chosen org", async () => {
238-
resolveOrgPrefetchedSpy.mockResolvedValue(null);
238+
resolveOrgSpy.mockResolvedValue(null);
239239
listOrgsSpy.mockResolvedValue([
240240
{ id: "1", slug: "org-a", name: "Org A" },
241241
{ id: "2", slug: "org-b", name: "Org B" },
@@ -249,7 +249,7 @@ describe("create-sentry-project", () => {
249249
});
250250

251251
test("multiple orgs + user cancels select throws WizardCancelledError", async () => {
252-
resolveOrgPrefetchedSpy.mockResolvedValue(null);
252+
resolveOrgSpy.mockResolvedValue(null);
253253
listOrgsSpy.mockResolvedValue([
254254
{ id: "1", slug: "org-a", name: "Org A" },
255255
{ id: "2", slug: "org-b", name: "Org B" },
@@ -295,23 +295,18 @@ describe("create-sentry-project", () => {
295295
expect(data.dsn).toBe("");
296296
});
297297

298-
describe("resolveOrgSlug — numeric org ID from DSN", () => {
299-
test("numeric ID + cache hit → resolved to slug", async () => {
300-
resolveOrgPrefetchedSpy.mockResolvedValue({ org: "4507492088676352" });
301-
getOrgByNumericIdSpy.mockReturnValue({
302-
slug: "acme-corp",
303-
regionUrl: "https://us.sentry.io",
304-
});
298+
describe("resolveOrgSlug — resolveOrg integration", () => {
299+
test("returns org from resolveOrg when it resolves", async () => {
300+
resolveOrgSpy.mockResolvedValue({ org: "acme-corp" });
305301

306302
const result = await resolveOrgSlug("/tmp/test", false);
307303

308304
expect(result).toBe("acme-corp");
309-
expect(getOrgByNumericIdSpy).toHaveBeenCalledWith("4507492088676352");
305+
expect(listOrgsSpy).not.toHaveBeenCalled();
310306
});
311307

312-
test("numeric ID + cache miss → falls through to single org in listOrganizations", async () => {
313-
resolveOrgPrefetchedSpy.mockResolvedValue({ org: "4507492088676352" });
314-
getOrgByNumericIdSpy.mockReturnValue(undefined);
308+
test("falls through to listOrganizations when resolveOrg returns null", async () => {
309+
resolveOrgSpy.mockResolvedValue(null);
315310
listOrgsSpy.mockResolvedValue([
316311
{ id: "1", slug: "solo-org", name: "Solo Org" },
317312
]);
@@ -320,22 +315,6 @@ describe("create-sentry-project", () => {
320315

321316
expect(result).toBe("solo-org");
322317
});
323-
324-
test("numeric ID + cache miss + multiple orgs + --yes → error with org list", async () => {
325-
resolveOrgPrefetchedSpy.mockResolvedValue({ org: "4507492088676352" });
326-
getOrgByNumericIdSpy.mockReturnValue(undefined);
327-
listOrgsSpy.mockResolvedValue([
328-
{ id: "1", slug: "org-a", name: "Org A" },
329-
{ id: "2", slug: "org-b", name: "Org B" },
330-
]);
331-
332-
const result = await resolveOrgSlug("/tmp/test", true);
333-
334-
expect(typeof result).toBe("object");
335-
const err = result as { ok: boolean; error: string };
336-
expect(err.ok).toBe(false);
337-
expect(err.error).toContain("Multiple organizations found");
338-
});
339318
});
340319

341320
describe("detectExistingProject (called directly)", () => {

0 commit comments

Comments
 (0)