From 1a135fb896fb84d8fe0fef9d58489459ef456b3b Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Sat, 1 Aug 2026 10:37:19 +0000 Subject: [PATCH 1/5] fix(alerts): create and edit issue alert rules via org-scoped /workflows/ Final slice of migrating issue alerts off the deprecated (HTTP 410 brownout) project-scoped /projects/{org}/{project}/rules/ endpoint (#1182), after read (#1215) and delete (#1216). This migrates create and edit. create/edit now go through the @sentry/api SDK (createOrganizationWorkflow / updateOrganizationWorkflow), assembling the workflow-document body: triggers {logicType, conditions}, actionFilters[{logicType, conditions, actions}], config.frequency, and detectorIds resolved from the project's error detector. --action-match/--filter-match map to logic types the same way the backend dual-write does (any -> any-short, all -> all). The SDK types conditions/actions as unknown[] and its body fields as snake_case, but the endpoint's CamelSnakeSerializer wants camelCase, so the body is built as a record and cast into the typed SDK arg (same double-cast pattern as getProject). No backend change needed. BREAKING CHANGE: --condition/--action/--filter now take workflow-native JSON ({type, comparison, conditionResult} / {type, data, config}) instead of legacy rule identifiers ({id: 'sentry.rules...'}). Refs #1182 --- .../cli/src/commands/alert/issues/create.ts | 64 +++++---- .../cli/src/commands/alert/issues/edit.ts | 72 ++++++---- .../cli/src/commands/alert/mutation-utils.ts | 10 ++ packages/cli/src/lib/api-client.ts | 5 +- packages/cli/src/lib/api/alerts.ts | 135 ++++++++++++------ .../test/commands/alert/issues/create.test.ts | 32 +++-- .../test/commands/alert/issues/edit.test.ts | 39 ++--- packages/cli/test/lib/api/alerts.test.ts | 80 +++++++++++ 8 files changed, 312 insertions(+), 125 deletions(-) diff --git a/packages/cli/src/commands/alert/issues/create.ts b/packages/cli/src/commands/alert/issues/create.ts index 190fcdfc77..0a7172f007 100644 --- a/packages/cli/src/commands/alert/issues/create.ts +++ b/packages/cli/src/commands/alert/issues/create.ts @@ -5,7 +5,10 @@ */ import type { SentryContext } from "../../../context.js"; -import { createIssueAlertRule } from "../../../lib/api-client.js"; +import { + createIssueAlertRule, + resolveErrorDetectorId, +} from "../../../lib/api-client.js"; import { parseOrgProjectArg } from "../../../lib/arg-parsing.js"; import { buildCommand, numberParser } from "../../../lib/command.js"; import { ContextError, ValidationError } from "../../../lib/errors.js"; @@ -13,6 +16,7 @@ import { CommandOutput } from "../../../lib/formatters/output.js"; import { DRY_RUN_ALIASES, DRY_RUN_FLAG } from "../../../lib/mutate-command.js"; import { resolveTargetsFromParsedArg } from "../../../lib/resolve-target.js"; import { + matchToLogicType, parseJsonObjectList, parseMatchMode, validateIssueRuleArrays, @@ -64,15 +68,20 @@ export const createCommand = buildCommand({ " --name, --condition (>=1), --action (>=1), --action-match all|any\n\n" + "Optional fields:\n" + " --frequency, --environment, --filter, --filter-match, --owner\n\n" + + "Conditions and actions are workflow-native JSON (this targets the\n" + + "org-scoped workflows endpoint):\n" + + " --condition a trigger data-condition: {type, comparison, conditionResult}\n" + + " --action an action: {type, data, config}\n" + + " --filter an action-filter condition (same shape as --condition)\n\n" + "Examples:\n" + - " sentry alert issues create my-org/my-app --name 'Error Spike' \\\n" + - ' --condition \'{"id":"sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}\' \\\n' + - ' --action \'{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}\' \\\n' + + " sentry alert issues create my-org/my-app --name 'New Issues' \\\n" + + ' --condition \'{"type":"first_seen_event","comparison":true,"conditionResult":true}\' \\\n' + + ' --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\' \\\n' + " --action-match any\n\n" + - " sentry alert issues create my-org/my-app --name 'Prod Errors' \\\n" + - ' --condition \'[{"id":"sentry.rules.conditions.every_event.EveryEventCondition"}]\' \\\n' + - ' --action \'[{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}]\' \\\n' + - " --action-match all --frequency 30 --dry-run", + " sentry alert issues create my-org/my-app --name 'High Priority' \\\n" + + ' --condition \'{"type":"new_high_priority_issue","comparison":true,"conditionResult":true}\' \\\n' + + ' --action \'{"type":"email","data":{},"config":{"targetType":"user","targetIdentifier":"56789"}}\' \\\n' + + " --frequency 30 --dry-run", }, output: { human: formatCreated, @@ -201,22 +210,31 @@ export const createCommand = buildCommand({ } const target = targets[0] as (typeof targets)[number]; + // Issue alerts fire on a project's error detector; connect via detector_ids. + const detectorId = await resolveErrorDetectorId(target.org, target.project); + + // Assemble the workflow-shaped body. The user supplies workflow-native + // conditions/actions; the CLI only builds the envelope (triggers + + // action_filters + logic types), mirroring the backend dual-write mapping. const body: Record = { name: flags.name, - conditions, - actions, - actionMatch: flags["action-match"], - frequency: flags.frequency, + detectorIds: [detectorId], + config: { frequency: flags.frequency }, + triggers: { + logicType: matchToLogicType(flags["action-match"]), + conditions, + }, + actionFilters: [ + { + logicType: matchToLogicType(flags["filter-match"]), + conditions: filters ?? [], + actions, + }, + ], }; if (flags.environment !== undefined) { body.environment = flags.environment; } - if (filters && filters.length > 0) { - body.filters = filters; - body.filterMatch = flags["filter-match"] ?? "all"; - } else if (flags["filter-match"] !== undefined) { - body.filterMatch = flags["filter-match"]; - } if (flags.owner !== undefined) { body.owner = flags.owner; } @@ -232,17 +250,15 @@ export const createCommand = buildCommand({ return { hint: "Dry run - no issue alert rule was created." }; } - const created = await createIssueAlertRule( - target.org, - target.project, - body - ); + const created = await createIssueAlertRule(target.org, body); yield new CommandOutput({ org: target.org, project: target.project, id: String(created.id ?? ""), name: String(created.name ?? flags.name), - status: String(created.status ?? "active"), + status: String( + created.status ?? (created.enabled === false ? "disabled" : "active") + ), } satisfies CreateResult); }, }); diff --git a/packages/cli/src/commands/alert/issues/edit.ts b/packages/cli/src/commands/alert/issues/edit.ts index 1d1f5e5ffe..93a60d17cd 100644 --- a/packages/cli/src/commands/alert/issues/edit.ts +++ b/packages/cli/src/commands/alert/issues/edit.ts @@ -7,8 +7,8 @@ import type { SentryContext } from "../../../context.js"; import { - getIssueAlertRuleDocument, - putIssueAlertRule, + getIssueAlertWorkflowDocument, + updateIssueAlertRule, } from "../../../lib/api-client.js"; import { parseOrgProjectArg } from "../../../lib/arg-parsing.js"; import { buildCommand, numberParser } from "../../../lib/command.js"; @@ -16,6 +16,7 @@ import { ContextError, ValidationError } from "../../../lib/errors.js"; import { CommandOutput } from "../../../lib/formatters/output.js"; import { resolveTargetsFromParsedArg } from "../../../lib/resolve-target.js"; import { + matchToLogicType, parseJsonObjectList, parseMatchMode, parseStatusFlag, @@ -87,34 +88,58 @@ function applyIssueEdits( body.name = flags.name; } if (flags.status !== undefined) { - body.status = flags.status; - } - if (conditions !== undefined) { - body.conditions = conditions; - } - if (actions !== undefined) { - body.actions = actions; - } - if (flags["action-match"] !== undefined) { - body.actionMatch = flags["action-match"]; + // Workflows use an `enabled` boolean rather than a status string. + body.enabled = flags.status === "active"; } if (flags.frequency !== undefined) { - body.frequency = flags.frequency; + const config = (body.config as Record | undefined) ?? {}; + config.frequency = flags.frequency; + body.config = config; } if (flags.environment !== undefined) { body.environment = flags.environment.trim() === "" ? null : flags.environment; } - if (filters !== undefined) { - body.filters = filters; - } - if (flags["filter-match"] !== undefined) { - body.filterMatch = flags["filter-match"]; - } if (flags.owner !== undefined) { body.owner = flags.owner.trim() === "" ? null : flags.owner; } + // Triggers: the "when" data-condition group. + if (conditions !== undefined || flags["action-match"] !== undefined) { + const triggers = + (body.triggers as Record | undefined) ?? {}; + if (conditions !== undefined) { + triggers.conditions = conditions; + } + if (flags["action-match"] !== undefined) { + triggers.logicType = matchToLogicType(flags["action-match"]); + } + body.triggers = triggers; + } + + // Action filter: the "if" group plus its actions. Issue alerts use one filter. + if ( + actions !== undefined || + filters !== undefined || + flags["filter-match"] !== undefined + ) { + const actionFilters = Array.isArray(body.actionFilters) + ? (body.actionFilters as Record[]) + : []; + const filter = (actionFilters[0] as Record) ?? {}; + if (actions !== undefined) { + filter.actions = actions; + } + if (filters !== undefined) { + filter.conditions = filters; + } + if (flags["filter-match"] !== undefined) { + filter.logicType = matchToLogicType(flags["filter-match"]); + } + actionFilters[0] = filter; + body.actionFilters = actionFilters; + } + if (conditions !== undefined) { validateIssueRuleArrays(conditions, actions, "conditions"); } @@ -248,16 +273,11 @@ export const editCommand = buildCommand({ ); const body = { - ...(await getIssueAlertRuleDocument(target.org, target.project, rule.id)), + ...(await getIssueAlertWorkflowDocument(target.org, rule.id)), } as Record; applyIssueEdits(body, flags); - const updated = await putIssueAlertRule( - target.org, - target.project, - rule.id, - body - ); + const updated = await updateIssueAlertRule(target.org, rule.id, body); yield new CommandOutput({ ...updated, org: target.org, diff --git a/packages/cli/src/commands/alert/mutation-utils.ts b/packages/cli/src/commands/alert/mutation-utils.ts index 077c689cb4..7a4cfff5b5 100644 --- a/packages/cli/src/commands/alert/mutation-utils.ts +++ b/packages/cli/src/commands/alert/mutation-utils.ts @@ -35,6 +35,16 @@ export function parseMatchMode( ); } +/** + * Map a legacy all|any match mode to a workflow DataConditionGroup logic type. + * Mirrors the backend issue-alert dual-write: "any" → "any-short", "all" → "all". + */ +export function matchToLogicType( + match: "all" | "any" | undefined +): "all" | "any-short" { + return match === "any" ? "any-short" : "all"; +} + /** Parse and validate an "active" | "disabled" status flag. Returns `undefined` when absent. */ export function parseStatusFlag( value: string | undefined diff --git a/packages/cli/src/lib/api-client.ts b/packages/cli/src/lib/api-client.ts index 162fd9478c..8c6f049468 100644 --- a/packages/cli/src/lib/api-client.ts +++ b/packages/cli/src/lib/api-client.ts @@ -26,15 +26,16 @@ export { deleteIssueAlertRule, deleteMetricAlertRule, getIssueAlertRule, - getIssueAlertRuleDocument, + getIssueAlertWorkflowDocument, getMetricAlertRule, getMetricAlertRuleDocument, type IssueAlertRule, listIssueAlertsPaginated, listMetricAlertsPaginated, type MetricAlertRule, - putIssueAlertRule, putMetricAlertRule, + resolveErrorDetectorId, + updateIssueAlertRule, } from "./api/alerts.js"; export { createDashboard, diff --git a/packages/cli/src/lib/api/alerts.ts b/packages/cli/src/lib/api/alerts.ts index 1a70196c11..84eda02a44 100644 --- a/packages/cli/src/lib/api/alerts.ts +++ b/packages/cli/src/lib/api/alerts.ts @@ -7,8 +7,11 @@ */ import { + createOrganizationWorkflow, getOrganizationDetector, + getOrganizationWorkflow, listOrganizationDetectors, + updateOrganizationWorkflow, } from "@sentry/api"; import { ApiError } from "../errors.js"; import { resolveOrgRegion } from "../region.js"; @@ -262,24 +265,59 @@ export async function listIssueAlertsPaginated( } /** - * Single GET for a project issue alert rule as full JSON (used as the edit - * baseline for PUT). - * - * NOTE: still uses the deprecated project-scoped `/rules/` endpoint. It backs - * the mutation path (`getIssueAlertRuleDocument` → edit), which is migrating to - * `/workflows/` in a follow-up (getsentry/cli#1182). + * Full workflow document for the edit baseline (name, config, environment, + * triggers, action_filters, detector_ids, ...), read from the org-scoped + * `/organizations/{org}/workflows/{id}/` detail endpoint. */ -async function fetchIssueAlertRuleJson( +export async function getIssueAlertWorkflowDocument( orgSlug: string, - projectSlug: string, - ruleId: string + workflowId: string ): Promise> { - const regionUrl = await resolveOrgRegion(orgSlug); - const { data } = await apiRequestToRegion>( - regionUrl, - `/projects/${orgSlug}/${projectSlug}/rules/${encodeURIComponent(ruleId)}/` + const config = await getOrgSdkConfig(orgSlug); + const result = await getOrganizationWorkflow({ + ...config, + path: { + organization_id_or_slug: orgSlug, + workflow_id: Number(workflowId), + }, + }); + return unwrapResult>( + result, + "Failed to fetch issue alert rule" ); - return data; +} + +/** + * Resolve the id of a project's error detector ("Error Monitor"). An issue alert + * workflow must connect to it via `detector_ids` to fire on new issues. Reads the + * org-scoped detectors endpoint filtered to the project and the `error` type. + * + * @throws {ApiError} 404 if the project has no error detector + */ +export async function resolveErrorDetectorId( + orgSlug: string, + projectSlug: string +): Promise { + const config = await getOrgSdkConfig(orgSlug); + const result = await listOrganizationDetectors({ + ...config, + path: { organization_id_or_slug: orgSlug }, + query: { project: [projectSlug], query: "type:error" }, + }); + const detectors = unwrapResult>( + result, + "Failed to resolve error detector" + ); + const detector = detectors[0]; + if (!detector) { + throw new ApiError( + `No error detector found for project '${projectSlug}'`, + 404, + undefined, + `/organizations/${orgSlug}/detectors/` + ); + } + return Number(detector.id); } /** @@ -452,49 +490,60 @@ export async function deleteIssueAlertRule( } /** - * Full document for PUT (includes conditions, actions, etc. from the API). - */ -export function getIssueAlertRuleDocument( - orgSlug: string, - projectSlug: string, - ruleId: string -): Promise> { - return fetchIssueAlertRuleJson(orgSlug, projectSlug, ruleId); -} - -/** - * Replace an issue alert rule (Sentry PUT is a full replacement). + * Replace (update) an issue alert workflow. Sentry PUT is a full replacement. + * + * `workflowId` is the id surfaced by the migrated read path; the update is keyed + * by id on the org-scoped `/workflows/{id}/` endpoint. */ -export async function putIssueAlertRule( +export async function updateIssueAlertRule( orgSlug: string, - projectSlug: string, - ruleId: string, + workflowId: string, body: Record ): Promise> { - const regionUrl = await resolveOrgRegion(orgSlug); - const { data } = await apiRequestToRegion>( - regionUrl, - `/projects/${orgSlug}/${projectSlug}/rules/${encodeURIComponent(ruleId)}/`, - { method: "PUT", body } + const config = await getOrgSdkConfig(orgSlug); + const result = await updateOrganizationWorkflow({ + ...config, + path: { + organization_id_or_slug: orgSlug, + workflow_id: Number(workflowId), + }, + // The SDK types conditions/actions as `unknown[]`; the CLI supplies the + // workflow-native body as a plain record, so cast into the typed arg. + body: body as unknown as Parameters< + typeof updateOrganizationWorkflow + >[0]["body"], + }); + return unwrapResult>( + result, + "Failed to update issue alert rule" ); - return data; } /** - * Create an issue (project) alert rule. + * Create an issue alert workflow on the org-scoped `/workflows/` endpoint. + * + * `body` must be workflow-shaped (name, detector_ids, config, triggers, + * action_filters, ...). Project linkage is carried by `detector_ids`, so no + * project slug is part of the request. */ export async function createIssueAlertRule( orgSlug: string, - projectSlug: string, body: Record ): Promise> { - const regionUrl = await resolveOrgRegion(orgSlug); - const { data } = await apiRequestToRegion>( - regionUrl, - `/projects/${orgSlug}/${projectSlug}/rules/`, - { method: "POST", body } + const config = await getOrgSdkConfig(orgSlug); + const result = await createOrganizationWorkflow({ + ...config, + path: { organization_id_or_slug: orgSlug }, + // The SDK types conditions/actions as `unknown[]`; the CLI supplies the + // workflow-native body as a plain record, so cast into the typed arg. + body: body as unknown as Parameters< + typeof createOrganizationWorkflow + >[0]["body"], + }); + return unwrapResult>( + result, + "Failed to create issue alert rule" ); - return data; } // Metric alert (org) write operations diff --git a/packages/cli/test/commands/alert/issues/create.test.ts b/packages/cli/test/commands/alert/issues/create.test.ts index c642dba51b..1912457b1c 100644 --- a/packages/cli/test/commands/alert/issues/create.test.ts +++ b/packages/cli/test/commands/alert/issues/create.test.ts @@ -46,15 +46,19 @@ function createContext() { describe("alert issues create", () => { let resolveSpy: ReturnType; let createSpy: ReturnType; + let detectorSpy: ReturnType; beforeEach(() => { resolveSpy = vi.spyOn(resolveTarget, "resolveTargetsFromParsedArg"); createSpy = vi.spyOn(apiClient, "createIssueAlertRule"); + detectorSpy = vi.spyOn(apiClient, "resolveErrorDetectorId"); + detectorSpy.mockResolvedValue(100); }); afterEach(() => { resolveSpy.mockRestore(); createSpy.mockRestore(); + detectorSpy.mockRestore(); }); test("requires --action-match", async () => { @@ -193,13 +197,17 @@ describe("alert issues create", () => { dryRun: true, body: { name: "Rule A", - conditions: [{ id: "condition-a" }], - actions: [{ id: "action-a" }], - actionMatch: "all", - frequency: 30, + detectorIds: [100], + config: { frequency: 30 }, + triggers: { logicType: "all", conditions: [{ id: "condition-a" }] }, + actionFilters: [ + { + logicType: "all", + conditions: [{ id: "filter-a" }], + actions: [{ id: "action-a" }], + }, + ], environment: "prod", - filters: [{ id: "filter-a" }], - filterMatch: "all", owner: "team:ops", }, }); @@ -270,12 +278,14 @@ describe("alert issues create", () => { "test-org/test-project" ); - expect(createSpy).toHaveBeenCalledWith("test-org", "test-project", { + expect(createSpy).toHaveBeenCalledWith("test-org", { name: "Rule A", - conditions: [{ id: "condition-a" }], - actions: [{ id: "action-a" }], - actionMatch: "any", - frequency: 15, + detectorIds: [100], + config: { frequency: 15 }, + triggers: { logicType: "any-short", conditions: [{ id: "condition-a" }] }, + actionFilters: [ + { logicType: "all", conditions: [], actions: [{ id: "action-a" }] }, + ], }); }); }); diff --git a/packages/cli/test/commands/alert/issues/edit.test.ts b/packages/cli/test/commands/alert/issues/edit.test.ts index 725d9b900f..f777b7373a 100644 --- a/packages/cli/test/commands/alert/issues/edit.test.ts +++ b/packages/cli/test/commands/alert/issues/edit.test.ts @@ -59,8 +59,8 @@ describe("alert issues edit", () => { beforeEach(() => { getRuleSpy = vi.spyOn(apiClient, "getIssueAlertRule"); - getDocSpy = vi.spyOn(apiClient, "getIssueAlertRuleDocument"); - putSpy = vi.spyOn(apiClient, "putIssueAlertRule"); + getDocSpy = vi.spyOn(apiClient, "getIssueAlertWorkflowDocument"); + putSpy = vi.spyOn(apiClient, "updateIssueAlertRule"); resolveSpy = vi.spyOn(resolveTarget, "resolveTargetsFromParsedArg"); }); @@ -84,27 +84,27 @@ describe("alert issues edit", () => { ).rejects.toBeInstanceOf(ValidationError); }); - test("merges additional fields into full PUT body", async () => { + test("merges additional fields into full workflow update body", async () => { const context = createContext(); resolveSpy.mockResolvedValue({ targets: [sampleTarget] }); getRuleSpy.mockResolvedValue(sampleRule); getDocSpy.mockResolvedValue({ id: "42", name: "Rule Alpha", - status: "active", - actionMatch: "any", - conditions: [{ id: "old-condition" }], - actions: [{ id: "old-action" }], - frequency: 30, + enabled: true, + config: { frequency: 30 }, + triggers: { + logicType: "any-short", + conditions: [{ id: "old-condition" }], + }, + actionFilters: [ + { logicType: "all", conditions: [], actions: [{ id: "old-action" }] }, + ], }); putSpy.mockResolvedValue({ id: "42", name: "Rule Beta", - status: "disabled", - actionMatch: "all", - conditions: [{ id: "new-condition" }], - actions: [{ id: "new-action" }], - frequency: 30, + enabled: false, }); const func = (await editCommand.loader()) as unknown as ( this: unknown, @@ -125,14 +125,15 @@ describe("alert issues edit", () => { "test-org/test-project/42" ); - expect(putSpy).toHaveBeenCalledWith("test-org", "test-project", "42", { + expect(putSpy).toHaveBeenCalledWith("test-org", "42", { id: "42", name: "Rule Beta", - status: "disabled", - actionMatch: "all", - conditions: [{ id: "new-condition" }], - actions: [{ id: "new-action" }], - frequency: 30, + enabled: false, + config: { frequency: 30 }, + triggers: { logicType: "all", conditions: [{ id: "new-condition" }] }, + actionFilters: [ + { logicType: "all", conditions: [], actions: [{ id: "new-action" }] }, + ], }); }); }); diff --git a/packages/cli/test/lib/api/alerts.test.ts b/packages/cli/test/lib/api/alerts.test.ts index db37ea993a..a01fd23b00 100644 --- a/packages/cli/test/lib/api/alerts.test.ts +++ b/packages/cli/test/lib/api/alerts.test.ts @@ -1,11 +1,15 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { + createIssueAlertRule, deleteIssueAlertRule, deleteMetricAlertRule, getIssueAlertRule, + getIssueAlertWorkflowDocument, getMetricAlertRule, listIssueAlertsPaginated, listMetricAlertsPaginated, + resolveErrorDetectorId, + updateIssueAlertRule, } from "../../../src/lib/api/alerts.js"; import { DEFAULT_SENTRY_URL } from "../../../src/lib/constants.js"; import { setAuthToken } from "../../../src/lib/db/auth.js"; @@ -279,6 +283,82 @@ describe("getMetricAlertRule", () => { }); }); +describe("createIssueAlertRule", () => { + test("POSTs the body to the org-scoped /workflows/ endpoint", async () => { + globalThis.fetch = mockFetch(async (input, init) => { + const req = new Request(input!, init); + expect(req.method).toBe("POST"); + expect(new URL(req.url).pathname).toBe( + "/api/0/organizations/test-org/workflows/" + ); + expect(await req.json()).toEqual({ name: "New", detectorIds: [7] }); + return Response.json(workflowRule({ id: "5", name: "New" })); + }); + + const created = await createIssueAlertRule("test-org", { + name: "New", + detectorIds: [7], + }); + expect(created.id).toBe("5"); + }); +}); + +describe("updateIssueAlertRule", () => { + test("PUTs the body to the /workflows/{id}/ endpoint", async () => { + globalThis.fetch = mockFetch(async (input, init) => { + const req = new Request(input!, init); + expect(req.method).toBe("PUT"); + expect(new URL(req.url).pathname).toBe( + "/api/0/organizations/test-org/workflows/42/" + ); + expect(await req.json()).toEqual({ name: "Renamed" }); + return Response.json(workflowRule({ id: "42", name: "Renamed" })); + }); + + const updated = await updateIssueAlertRule("test-org", "42", { + name: "Renamed", + }); + expect(updated.name).toBe("Renamed"); + }); +}); + +describe("getIssueAlertWorkflowDocument", () => { + test("reads the single workflow detail endpoint", async () => { + globalThis.fetch = mockFetch(async (input, init) => { + const url = new URL(new Request(input!, init).url); + expect(url.pathname).toBe("/api/0/organizations/test-org/workflows/42/"); + return Response.json(workflowRule({ id: "42" })); + }); + + const doc = await getIssueAlertWorkflowDocument("test-org", "42"); + expect(doc.id).toBe("42"); + }); +}); + +describe("resolveErrorDetectorId", () => { + test("returns the id of the project's error detector", async () => { + globalThis.fetch = mockFetch(async (input, init) => { + const url = new URL(new Request(input!, init).url); + expect(url.pathname).toBe("/api/0/organizations/test-org/detectors/"); + expect(url.searchParams.get("project")).toBe("test-project"); + expect(url.searchParams.get("query")).toBe("type:error"); + return Response.json([{ id: 7, type: "error" }]); + }); + + await expect( + resolveErrorDetectorId("test-org", "test-project") + ).resolves.toBe(7); + }); + + test("throws 404 ApiError when the project has no error detector", async () => { + globalThis.fetch = mockFetch(async () => Response.json([])); + + await expect( + resolveErrorDetectorId("test-org", "test-project") + ).rejects.toMatchObject({ name: "ApiError", status: 404 }); + }); +}); + describe("deleteMetricAlertRule", () => { test("treats empty-body 202 as success", async () => { globalThis.fetch = mockFetch(async (input, init) => { From 1db256cf2edfd18d6867d45e32bffcfea25f44c1 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Sat, 1 Aug 2026 10:50:30 +0000 Subject: [PATCH 2/5] fix(alerts): map workflow enabled field to status in edit output The workflows update endpoint returns `enabled` instead of `status`, but `edit` spread the raw response so human output showed `undefined` and JSON dropped `status`. Mirror the create path's enabled->status mapping. --- .../cli/src/commands/alert/issues/edit.ts | 5 ++++ .../test/commands/alert/issues/edit.test.ts | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/packages/cli/src/commands/alert/issues/edit.ts b/packages/cli/src/commands/alert/issues/edit.ts index 93a60d17cd..ab3c96daa2 100644 --- a/packages/cli/src/commands/alert/issues/edit.ts +++ b/packages/cli/src/commands/alert/issues/edit.ts @@ -283,6 +283,11 @@ export const editCommand = buildCommand({ org: target.org, project: target.project, id: String(updated.id ?? rule.id), + // The workflows endpoint returns `enabled` rather than `status`; map it + // back to a status label so human/JSON output matches the create path. + status: String( + updated.status ?? (updated.enabled === false ? "disabled" : "active") + ), } satisfies EditResult); }, }); diff --git a/packages/cli/test/commands/alert/issues/edit.test.ts b/packages/cli/test/commands/alert/issues/edit.test.ts index f777b7373a..aaef593062 100644 --- a/packages/cli/test/commands/alert/issues/edit.test.ts +++ b/packages/cli/test/commands/alert/issues/edit.test.ts @@ -136,4 +136,30 @@ describe("alert issues edit", () => { ], }); }); + + test("maps the workflow enabled field to a status label in output", async () => { + const context = createContext(); + resolveSpy.mockResolvedValue({ targets: [sampleTarget] }); + getRuleSpy.mockResolvedValue(sampleRule); + getDocSpy.mockResolvedValue({ + id: "42", + name: "Rule Alpha", + enabled: true, + }); + putSpy.mockResolvedValue({ id: "42", name: "Rule Alpha", enabled: false }); + const func = (await editCommand.loader()) as unknown as ( + this: unknown, + flags: EditFlags, + arg: string + ) => Promise; + + await func.call( + context, + { status: "disabled", json: true }, + "test-org/test-project/42" + ); + + const output = context.stdout.write.mock.calls.map((c) => c[0]).join(""); + expect(JSON.parse(output)).toMatchObject({ id: "42", status: "disabled" }); + }); }); From 5b858e8ec7d0eaea09bff9df49f30bbb197b7baf Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Sat, 1 Aug 2026 16:18:38 +0000 Subject: [PATCH 3/5] fix(alerts): force issue-alert trigger logic type to any-short MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The org-scoped workflows create endpoint rejects a trigger group that carries an issue-alert trigger condition (first_seen_event / regression_event / reappeared_event / issue_resolved_trigger) unless its logic type is 'any-short' (BaseDataConditionGroupValidator._validate_logic_type in getsentry/sentry). We were mapping --action-match all -> logicType 'all' on the trigger group, so create would fail for the standard issue-alert triggers. The dual-write in the backend preserves 'all' via .objects.create(), which bypasses this validator — those groups are explicitly grandfathered, so the CLI's endpoint path can't rely on that. Split the mapping: triggerLogicType() always returns 'any-short' for the trigger ("when") group; matchToLogicType() still maps --filter-match for the action-filter ("if") group, which never holds trigger conditions and accepts either logic type. edit does the same so it never writes a non-any-short trigger. --action-match stays for parity but no longer affects the trigger. --- .../cli/src/commands/alert/issues/create.ts | 7 +++++- .../cli/src/commands/alert/issues/edit.ts | 3 ++- .../cli/src/commands/alert/mutation-utils.ts | 24 +++++++++++++++++-- .../test/commands/alert/issues/create.test.ts | 5 +++- .../test/commands/alert/issues/edit.test.ts | 5 +++- .../commands/alert/mutation-utils.test.ts | 22 +++++++++++++++++ 6 files changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/alert/issues/create.ts b/packages/cli/src/commands/alert/issues/create.ts index 0a7172f007..c27f3c2172 100644 --- a/packages/cli/src/commands/alert/issues/create.ts +++ b/packages/cli/src/commands/alert/issues/create.ts @@ -19,6 +19,7 @@ import { matchToLogicType, parseJsonObjectList, parseMatchMode, + triggerLogicType, validateIssueRuleArrays, } from "../mutation-utils.js"; @@ -73,6 +74,10 @@ export const createCommand = buildCommand({ " --condition a trigger data-condition: {type, comparison, conditionResult}\n" + " --action an action: {type, data, config}\n" + " --filter an action-filter condition (same shape as --condition)\n\n" + + "Match modes: --filter-match all|any controls the action-filter group.\n" + + "--action-match is accepted for parity but the trigger group is always\n" + + "evaluated as 'any-short' — issue alerts fire on a single error detector,\n" + + "and the workflows endpoint requires that logic type for issue triggers.\n\n" + "Examples:\n" + " sentry alert issues create my-org/my-app --name 'New Issues' \\\n" + ' --condition \'{"type":"first_seen_event","comparison":true,"conditionResult":true}\' \\\n' + @@ -221,7 +226,7 @@ export const createCommand = buildCommand({ detectorIds: [detectorId], config: { frequency: flags.frequency }, triggers: { - logicType: matchToLogicType(flags["action-match"]), + logicType: triggerLogicType(), conditions, }, actionFilters: [ diff --git a/packages/cli/src/commands/alert/issues/edit.ts b/packages/cli/src/commands/alert/issues/edit.ts index ab3c96daa2..fb78b583cb 100644 --- a/packages/cli/src/commands/alert/issues/edit.ts +++ b/packages/cli/src/commands/alert/issues/edit.ts @@ -20,6 +20,7 @@ import { parseJsonObjectList, parseMatchMode, parseStatusFlag, + triggerLogicType, validateIssueRuleArrays, } from "../mutation-utils.js"; import { parseIssueRuleArg, resolveIssueAlertRule } from "./rule-resolve.js"; @@ -112,7 +113,7 @@ function applyIssueEdits( triggers.conditions = conditions; } if (flags["action-match"] !== undefined) { - triggers.logicType = matchToLogicType(flags["action-match"]); + triggers.logicType = triggerLogicType(); } body.triggers = triggers; } diff --git a/packages/cli/src/commands/alert/mutation-utils.ts b/packages/cli/src/commands/alert/mutation-utils.ts index 7a4cfff5b5..13ac8eb3ee 100644 --- a/packages/cli/src/commands/alert/mutation-utils.ts +++ b/packages/cli/src/commands/alert/mutation-utils.ts @@ -36,8 +36,12 @@ export function parseMatchMode( } /** - * Map a legacy all|any match mode to a workflow DataConditionGroup logic type. - * Mirrors the backend issue-alert dual-write: "any" → "any-short", "all" → "all". + * Map an action-filter match mode to a workflow DataConditionGroup logic type. + * + * Applies to the **action-filter** ("if") group only. Mirrors the backend + * issue-alert dual-write: "any" → "any-short", "all" → "all". The filter group + * never holds trigger-type conditions, so the workflows create validator + * accepts either logic type here. */ export function matchToLogicType( match: "all" | "any" | undefined @@ -45,6 +49,22 @@ export function matchToLogicType( return match === "any" ? "any-short" : "all"; } +/** + * Logic type for an issue alert's **trigger** ("when") DataConditionGroup. + * + * Always "any-short". The org-scoped workflows create endpoint rejects a + * trigger group that carries an issue-alert trigger condition + * (first_seen_event / regression_event / reappeared_event / + * issue_resolved_trigger) with any other logic type — see + * `BaseDataConditionGroupValidator._validate_logic_type` in getsentry/sentry. + * An issue alert attaches to one error detector, so `all` vs `any` on the + * trigger group is not a meaningful choice; use `--filter-match` to control + * the action-filter group's match mode instead. + */ +export function triggerLogicType(): "any-short" { + return "any-short"; +} + /** Parse and validate an "active" | "disabled" status flag. Returns `undefined` when absent. */ export function parseStatusFlag( value: string | undefined diff --git a/packages/cli/test/commands/alert/issues/create.test.ts b/packages/cli/test/commands/alert/issues/create.test.ts index 1912457b1c..2ec65142c0 100644 --- a/packages/cli/test/commands/alert/issues/create.test.ts +++ b/packages/cli/test/commands/alert/issues/create.test.ts @@ -199,7 +199,10 @@ describe("alert issues create", () => { name: "Rule A", detectorIds: [100], config: { frequency: 30 }, - triggers: { logicType: "all", conditions: [{ id: "condition-a" }] }, + triggers: { + logicType: "any-short", + conditions: [{ id: "condition-a" }], + }, actionFilters: [ { logicType: "all", diff --git a/packages/cli/test/commands/alert/issues/edit.test.ts b/packages/cli/test/commands/alert/issues/edit.test.ts index aaef593062..864f581ad1 100644 --- a/packages/cli/test/commands/alert/issues/edit.test.ts +++ b/packages/cli/test/commands/alert/issues/edit.test.ts @@ -130,7 +130,10 @@ describe("alert issues edit", () => { name: "Rule Beta", enabled: false, config: { frequency: 30 }, - triggers: { logicType: "all", conditions: [{ id: "new-condition" }] }, + triggers: { + logicType: "any-short", + conditions: [{ id: "new-condition" }], + }, actionFilters: [ { logicType: "all", conditions: [], actions: [{ id: "new-action" }] }, ], diff --git a/packages/cli/test/commands/alert/mutation-utils.test.ts b/packages/cli/test/commands/alert/mutation-utils.test.ts index 0d28134c7d..3e9b9d1d66 100644 --- a/packages/cli/test/commands/alert/mutation-utils.test.ts +++ b/packages/cli/test/commands/alert/mutation-utils.test.ts @@ -1,10 +1,12 @@ import { describe, expect, test } from "vitest"; import { + matchToLogicType, normalizeProjectList, parseJsonObjectList, parseMatchMode, parseStatusFlag, statusToMetricValue, + triggerLogicType, validateIssueRuleArrays, validateMetricDataset, validateMetricTimeWindow, @@ -41,6 +43,26 @@ describe("parseMatchMode", () => { }); }); +describe("matchToLogicType", () => { + test('maps "any" to "any-short"', () => { + expect(matchToLogicType("any")).toBe("any-short"); + }); + + test('maps "all" to "all"', () => { + expect(matchToLogicType("all")).toBe("all"); + }); + + test('defaults undefined to "all"', () => { + expect(matchToLogicType(undefined)).toBe("all"); + }); +}); + +describe("triggerLogicType", () => { + test("is always any-short (workflows endpoint rejects other logic types for issue triggers)", () => { + expect(triggerLogicType()).toBe("any-short"); + }); +}); + describe("parseStatusFlag", () => { test("returns undefined for undefined", () => { expect(parseStatusFlag(undefined)).toBeUndefined(); From 8323c7023a2347ed4f80df4bf39811ba94ac11b3 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Sat, 1 Aug 2026 16:39:36 +0000 Subject: [PATCH 4/5] fix(alerts): drop the no-op --action-match flag from issue create/edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the trigger group is always 'any-short' for issue alerts, --action-match had no effect on the workflows body — it was accepted (and, on create, required) but silently ignored. Remove it entirely: drop the flag, its -m alias, the required-flag guard on create, and the type/help references. The trigger group's logicType is now pinned to any-short whenever conditions are set. --filter-match (all/any) on the action-filter group — the meaningful AND/OR control — is unchanged. Also refresh the alert docs fragment: the create example dropped --action-match and switched to workflow-native condition/action JSON to match this command's post-migration surface. --- apps/cli-docs/src/fragments/commands/alert.md | 5 ++- .../skills/sentry-cli/references/alert.md | 7 ++--- .../cli/src/commands/alert/issues/create.ts | 28 ++++------------- .../cli/src/commands/alert/issues/edit.ts | 23 ++++---------- .../test/commands/alert/issues/create.test.ts | 31 ------------------- .../test/commands/alert/issues/edit.test.ts | 2 -- 6 files changed, 16 insertions(+), 80 deletions(-) diff --git a/apps/cli-docs/src/fragments/commands/alert.md b/apps/cli-docs/src/fragments/commands/alert.md index 8f0630f619..1d47e382b0 100644 --- a/apps/cli-docs/src/fragments/commands/alert.md +++ b/apps/cli-docs/src/fragments/commands/alert.md @@ -8,9 +8,8 @@ # Create an issue alert rule with inline JSON condition/action sentry alert issues create my-org/my-project \ --name "Error Spike" \ - --condition '{"id":"sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}' \ - --action '{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}' \ - --action-match any + --condition '{"type":"first_seen_event","comparison":true,"conditionResult":true}' \ + --action '{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}' ``` ### List issue alert rules diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md index e9fabefe43..ec9e434803 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md @@ -57,7 +57,6 @@ Create an issue alert rule - `--name - Rule name` - `-c, --condition ... - Condition object JSON (repeatable, or pass one JSON array)` - `-a, --action ... - Action object JSON (repeatable, or pass one JSON array)` -- `-m, --action-match - Condition/action match mode: all or any` - `--frequency - Frequency in minutes (default: 30) - (default: 30)` - `--environment - Environment filter` - `--filter ... - Filter object JSON (repeatable, or pass one JSON array)` @@ -71,9 +70,8 @@ Create an issue alert rule # Create an issue alert rule with inline JSON condition/action sentry alert issues create my-org/my-project \ --name "Error Spike" \ - --condition '{"id":"sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}' \ - --action '{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}' \ - --action-match any + --condition '{"type":"first_seen_event","comparison":true,"conditionResult":true}' \ + --action '{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}' ``` ### `sentry alert issues delete ` @@ -101,7 +99,6 @@ Edit an issue alert rule - `--status - Rule status: active or disabled` - `-c, --condition ... - Condition object JSON (repeatable, or pass one JSON array)` - `-a, --action ... - Action object JSON (repeatable, or pass one JSON array)` -- `-m, --action-match - Condition/action match mode: all or any` - `--frequency - Frequency in minutes` - `--environment - Environment value (pass empty string to clear)` - `--filter ... - Filter object JSON (repeatable, or pass one JSON array)` diff --git a/packages/cli/src/commands/alert/issues/create.ts b/packages/cli/src/commands/alert/issues/create.ts index c27f3c2172..64e479aca5 100644 --- a/packages/cli/src/commands/alert/issues/create.ts +++ b/packages/cli/src/commands/alert/issues/create.ts @@ -24,13 +24,12 @@ import { } from "../mutation-utils.js"; const USAGE_HINT = - "sentry alert issues create --name --condition --action --action-match all|any"; + "sentry alert issues create --name --condition --action "; type CreateFlags = { readonly name: string; readonly condition?: string[]; readonly action?: string[]; - readonly "action-match"?: "all" | "any"; readonly frequency: number; readonly environment?: string; readonly filter?: string[]; @@ -66,7 +65,7 @@ export const createCommand = buildCommand({ "/, an auto-detected project, or a bare project search when " + "it resolves to exactly one project.\n\n" + "Required fields:\n" + - " --name, --condition (>=1), --action (>=1), --action-match all|any\n\n" + + " --name, --condition (>=1), --action (>=1)\n\n" + "Optional fields:\n" + " --frequency, --environment, --filter, --filter-match, --owner\n\n" + "Conditions and actions are workflow-native JSON (this targets the\n" + @@ -74,15 +73,13 @@ export const createCommand = buildCommand({ " --condition a trigger data-condition: {type, comparison, conditionResult}\n" + " --action an action: {type, data, config}\n" + " --filter an action-filter condition (same shape as --condition)\n\n" + - "Match modes: --filter-match all|any controls the action-filter group.\n" + - "--action-match is accepted for parity but the trigger group is always\n" + - "evaluated as 'any-short' — issue alerts fire on a single error detector,\n" + - "and the workflows endpoint requires that logic type for issue triggers.\n\n" + + "Match mode: --filter-match all|any controls how the action-filter\n" + + "conditions combine. Issue-alert triggers always evaluate as 'any-short'\n" + + "(they fire on a single error detector), so there is no trigger match flag.\n\n" + "Examples:\n" + " sentry alert issues create my-org/my-app --name 'New Issues' \\\n" + ' --condition \'{"type":"first_seen_event","comparison":true,"conditionResult":true}\' \\\n' + - ' --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\' \\\n' + - " --action-match any\n\n" + + ' --action \'{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}\'\n\n' + " sentry alert issues create my-org/my-app --name 'High Priority' \\\n" + ' --condition \'{"type":"new_high_priority_issue","comparison":true,"conditionResult":true}\' \\\n' + ' --action \'{"type":"email","data":{},"config":{"targetType":"user","targetIdentifier":"56789"}}\' \\\n' + @@ -125,12 +122,6 @@ export const createCommand = buildCommand({ optional: true, brief: "Action object JSON (repeatable, or pass one JSON array)", }, - "action-match": { - kind: "parsed", - parse: (value: string) => parseMatchMode(value, "action-match"), - optional: true, - brief: "Condition/action match mode: all or any", - }, frequency: { kind: "parsed", parse: numberParser, @@ -168,7 +159,6 @@ export const createCommand = buildCommand({ ...DRY_RUN_ALIASES, c: "condition", a: "action", - m: "action-match", }, }, async *func( @@ -186,12 +176,6 @@ export const createCommand = buildCommand({ "frequency" ); } - if (!flags["action-match"]) { - throw new ValidationError( - "Pass --action-match with one of: all, any.", - "action-match" - ); - } const conditions = parseJsonObjectList(flags.condition, "condition"); const actions = parseJsonObjectList(flags.action, "action"); diff --git a/packages/cli/src/commands/alert/issues/edit.ts b/packages/cli/src/commands/alert/issues/edit.ts index fb78b583cb..461d5f38eb 100644 --- a/packages/cli/src/commands/alert/issues/edit.ts +++ b/packages/cli/src/commands/alert/issues/edit.ts @@ -33,7 +33,6 @@ type EditFlags = { readonly status?: "active" | "disabled" | undefined; readonly condition?: string[]; readonly action?: string[]; - readonly "action-match"?: "all" | "any"; readonly frequency?: number; readonly environment?: string; readonly filter?: string[]; @@ -55,7 +54,6 @@ function hasIssueMutations(flags: EditFlags): boolean { flags.status !== undefined || flags.condition !== undefined || flags.action !== undefined || - flags["action-match"] !== undefined || flags.frequency !== undefined || flags.environment !== undefined || flags.filter !== undefined || @@ -105,16 +103,14 @@ function applyIssueEdits( body.owner = flags.owner.trim() === "" ? null : flags.owner; } - // Triggers: the "when" data-condition group. - if (conditions !== undefined || flags["action-match"] !== undefined) { + // Triggers: the "when" data-condition group. Issue-alert triggers always use + // the 'any-short' logic type (see triggerLogicType), so we pin it whenever the + // conditions change rather than exposing a trigger match flag. + if (conditions !== undefined) { const triggers = (body.triggers as Record | undefined) ?? {}; - if (conditions !== undefined) { - triggers.conditions = conditions; - } - if (flags["action-match"] !== undefined) { - triggers.logicType = triggerLogicType(); - } + triggers.conditions = conditions; + triggers.logicType = triggerLogicType(); body.triggers = triggers; } @@ -209,12 +205,6 @@ export const editCommand = buildCommand({ optional: true, brief: "Action object JSON (repeatable, or pass one JSON array)", }, - "action-match": { - kind: "parsed", - parse: (value: string) => parseMatchMode(value, "action-match"), - optional: true, - brief: "Condition/action match mode: all or any", - }, frequency: { kind: "parsed", parse: numberParser, @@ -250,7 +240,6 @@ export const editCommand = buildCommand({ aliases: { c: "condition", a: "action", - m: "action-match", }, }, async *func(this: SentryContext, flags: EditFlags, arg: string) { diff --git a/packages/cli/test/commands/alert/issues/create.test.ts b/packages/cli/test/commands/alert/issues/create.test.ts index 2ec65142c0..8c510a0623 100644 --- a/packages/cli/test/commands/alert/issues/create.test.ts +++ b/packages/cli/test/commands/alert/issues/create.test.ts @@ -23,7 +23,6 @@ type CreateFlags = { readonly name: string; readonly condition?: string[]; readonly action?: string[]; - readonly "action-match"?: "all" | "any"; readonly frequency: number; readonly environment?: string; readonly filter?: string[]; @@ -61,30 +60,6 @@ describe("alert issues create", () => { detectorSpy.mockRestore(); }); - test("requires --action-match", async () => { - const context = createContext(); - const func = (await createCommand.loader()) as unknown as ( - this: unknown, - flags: CreateFlags, - arg: string - ) => Promise; - - await expect( - func.call( - context, - { - name: "Rule A", - condition: ['{"id":"condition-a"}'], - action: ['{"id":"action-a"}'], - frequency: 30, - "dry-run": true, - json: true, - }, - "test-org/test-project" - ) - ).rejects.toBeInstanceOf(ValidationError); - }); - test("rejects blank rule name", async () => { const context = createContext(); const func = (await createCommand.loader()) as unknown as ( @@ -100,7 +75,6 @@ describe("alert issues create", () => { name: " ", condition: ['{"id":"condition-a"}'], action: ['{"id":"action-a"}'], - "action-match": "any", frequency: 30, "dry-run": true, json: true, @@ -125,7 +99,6 @@ describe("alert issues create", () => { name: "Rule A", condition: ['{"id":"condition-a"}'], action: ['{"id":"action-a"}'], - "action-match": "any", frequency: 0, "dry-run": true, json: true, @@ -150,7 +123,6 @@ describe("alert issues create", () => { name: "Rule A", condition: ['{"id":"condition-a"}'], action: ['{"id":"action-a"}'], - "action-match": "all", frequency: 30, "dry-run": true, json: true, @@ -176,7 +148,6 @@ describe("alert issues create", () => { name: "Rule A", condition: ['{"id":"condition-a"}'], action: ['{"id":"action-a"}'], - "action-match": "all", frequency: 30, environment: "prod", filter: ['{"id":"filter-a"}'], @@ -242,7 +213,6 @@ describe("alert issues create", () => { name: "Rule A", condition: ['{"id":"condition-a"}'], action: ['{"id":"action-a"}'], - "action-match": "any", frequency: 30, "dry-run": true, json: true, @@ -273,7 +243,6 @@ describe("alert issues create", () => { name: "Rule A", condition: ['{"id":"condition-a"}'], action: ['{"id":"action-a"}'], - "action-match": "any", frequency: 15, "dry-run": false, json: true, diff --git a/packages/cli/test/commands/alert/issues/edit.test.ts b/packages/cli/test/commands/alert/issues/edit.test.ts index 864f581ad1..8f0a2615a4 100644 --- a/packages/cli/test/commands/alert/issues/edit.test.ts +++ b/packages/cli/test/commands/alert/issues/edit.test.ts @@ -39,7 +39,6 @@ type EditFlags = { readonly status?: "active" | "disabled"; readonly condition?: string[]; readonly action?: string[]; - readonly "action-match"?: "all" | "any"; readonly json: boolean; }; @@ -119,7 +118,6 @@ describe("alert issues edit", () => { status: "disabled", condition: ['{"id":"new-condition"}'], action: ['{"id":"new-action"}'], - "action-match": "all", json: true, }, "test-org/test-project/42" From 83ae6c6230d637b93d17d50f73f850adc5cd65a1 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Sat, 1 Aug 2026 16:59:14 +0000 Subject: [PATCH 5/5] feat(alerts): alias -m to --filter-match on issue create/edit The -m short was freed up when --action-match was dropped; point it at --filter-match, the remaining match-mode flag. Regenerated skill docs. --- .../plugins/sentry-cli/skills/sentry-cli/references/alert.md | 4 ++-- packages/cli/src/commands/alert/issues/create.ts | 1 + packages/cli/src/commands/alert/issues/edit.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md index ec9e434803..cd0ee82cb2 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md @@ -60,7 +60,7 @@ Create an issue alert rule - `--frequency - Frequency in minutes (default: 30) - (default: 30)` - `--environment - Environment filter` - `--filter ... - Filter object JSON (repeatable, or pass one JSON array)` -- `--filter-match - Filter match mode: all or any` +- `-m, --filter-match - Filter match mode: all or any` - `--owner - Owner (team:user style value accepted by Sentry API)` - `-n, --dry-run - Show what would happen without making changes` @@ -102,7 +102,7 @@ Edit an issue alert rule - `--frequency - Frequency in minutes` - `--environment - Environment value (pass empty string to clear)` - `--filter ... - Filter object JSON (repeatable, or pass one JSON array)` -- `--filter-match - Filter match mode: all or any` +- `-m, --filter-match - Filter match mode: all or any` - `--owner - Owner value (pass empty string to clear)` **Examples:** diff --git a/packages/cli/src/commands/alert/issues/create.ts b/packages/cli/src/commands/alert/issues/create.ts index 64e479aca5..cadbfa8397 100644 --- a/packages/cli/src/commands/alert/issues/create.ts +++ b/packages/cli/src/commands/alert/issues/create.ts @@ -159,6 +159,7 @@ export const createCommand = buildCommand({ ...DRY_RUN_ALIASES, c: "condition", a: "action", + m: "filter-match", }, }, async *func( diff --git a/packages/cli/src/commands/alert/issues/edit.ts b/packages/cli/src/commands/alert/issues/edit.ts index 461d5f38eb..94e32508e6 100644 --- a/packages/cli/src/commands/alert/issues/edit.ts +++ b/packages/cli/src/commands/alert/issues/edit.ts @@ -240,6 +240,7 @@ export const editCommand = buildCommand({ aliases: { c: "condition", a: "action", + m: "filter-match", }, }, async *func(this: SentryContext, flags: EditFlags, arg: string) {