Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/bulk-replay-region-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Add an "Override region" option to the bulk replay action so replayed runs can be routed to a chosen region, defaulting to keeping each run in its original region.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton";
import { Select, SelectItem } from "~/components/primitives/Select";
import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
Expand All @@ -51,6 +52,7 @@ import { resolveOrgIdFromSlug } from "~/models/organization.server";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { CreateBulkActionPresenter } from "~/presenters/v3/CreateBulkActionPresenter.server";
import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server";
import { RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList";
import { logger } from "~/services/logger.server";
import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
Expand Down Expand Up @@ -82,20 +84,27 @@ export const loader = dashboardLoader(
}

const presenter = new CreateBulkActionPresenter();
const data = await presenter.call({
organizationId: project.organizationId,
projectId: project.id,
environmentId: environment.id,
request,
});
const [data, regionsResult] = await Promise.all([
presenter.call({
organizationId: project.organizationId,
projectId: project.id,
environmentId: environment.id,
request,
}),
new RegionsPresenter().call({
userId: user.id,
projectSlug: projectParam,
isAdmin: user.admin || user.isImpersonating,
}),
]);

// Display flag for the inspector's Cancel/Replay controls — the action
// below enforces write:runs independently.
const { canCreateBulkAction } = checkPermissions(ability, {
canCreateBulkAction: { action: "write", resource: { type: "runs" } },
});

return typedjson({ ...data, canCreateBulkAction });
return typedjson({ ...data, regions: regionsResult.regions, canCreateBulkAction });
}
);

Expand All @@ -104,6 +113,10 @@ export const CreateBulkActionSearchParams = z.object({
action: BulkActionAction.default("cancel"),
});

// Sentinel for the "Override region" dropdown meaning "keep each run's original
// region". Normalized to `undefined` in the action so the service never sees it.
const REPLAY_REGION_NO_OVERRIDE_VALUE = "__no_override__";

export const CreateBulkActionPayload = z.discriminatedUnion("mode", [
z.object({
mode: z.literal("selected"),
Expand All @@ -114,13 +127,15 @@ export const CreateBulkActionPayload = z.discriminatedUnion("mode", [
return [];
}, z.array(z.string())),
title: z.string().optional(),
region: z.string().optional(),
failedRedirect: z.string(),
emailNotification: z.preprocess((value) => value === "on", z.boolean()),
}),
z.object({
mode: z.literal("filter"),
action: BulkActionAction,
title: z.string().optional(),
region: z.string().optional(),
failedRedirect: z.string(),
emailNotification: z.preprocess((value) => value === "on", z.boolean()),
}),
Expand Down Expand Up @@ -160,6 +175,12 @@ export const action = dashboardAction(
return redirectWithErrorMessage("/", request, "Invalid bulk action");
}

// "Don't override" keeps each run's original region — drop it so it isn't
// stored as a real override.
if (submission.value.region === REPLAY_REGION_NO_OVERRIDE_VALUE) {
submission.value.region = undefined;
}
Comment on lines +178 to +182

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 No server-side validation that the submitted region is in the allowed list

The action handler at lines 180-182 normalizes the __no_override__ sentinel but does not validate that a non-sentinel region value is actually in the user's allowed region list. A crafted form submission could pass an arbitrary region string. However, this is consistent with the single-run replay path (resources.taskruns.$runParam.replay.ts:430) which also passes submission.value.region directly to ReplayTaskRunService without validation. The downstream TriggerTaskService likely rejects invalid regions at queue resolution time, but adding an explicit check in the action would be a defense-in-depth improvement.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


const service = new BulkActionService();
const [error, result] = await tryCatch(
service.create(
Expand Down Expand Up @@ -238,6 +259,21 @@ export function CreateBulkActionInspector({
const impactedCountElement =
mode === "selected" ? selectedItems.size : <EstimatedCount count={data?.count} />;

// Region is a replay-only override and only applies to deployed environments.
// The default keeps each run in its original region so a bulk action spanning
// multiple regions doesn't silently re-route runs.
const regions = data?.regions ?? [];
const showRegion =
action === "replay" && environment.type !== "DEVELOPMENT" && regions.length > 1;
const regionItems = [
{ value: REPLAY_REGION_NO_OVERRIDE_VALUE, label: "Don't override", isDefault: false },
...regions.map((r) => ({
value: r.name,
label: r.description ? `${r.name} — ${r.description}` : r.name,
Comment on lines +268 to +272

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the region routing key (masterQueue) as the submitted override value.

Line 245 currently posts r.name (display label), but replay routing consumes a queue/region key. This can misroute or fail replay overrides.

Suggested fix
   const regionItems = [
     { value: REPLAY_REGION_NO_OVERRIDE_VALUE, label: "Don't override", isDefault: false },
     ...regions.map((r) => ({
-      value: r.name,
+      value: r.masterQueue,
       label: r.description ? `${r.name} — ${r.description}` : r.name,
       isDefault: r.isDefault,
     })),
   ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const regionItems = [
{ value: REPLAY_REGION_NO_OVERRIDE_VALUE, label: "Don't override", isDefault: false },
...regions.map((r) => ({
value: r.name,
label: r.description ? `${r.name}${r.description}` : r.name,
const regionItems = [
{ value: REPLAY_REGION_NO_OVERRIDE_VALUE, label: "Don't override", isDefault: false },
...regions.map((r) => ({
value: r.masterQueue,
label: r.description ? `${r.name}${r.description}` : r.name,
isDefault: r.isDefault,
})),
];

isDefault: r.isDefault,
})),
];

return (
<Form
method="post"
Expand Down Expand Up @@ -368,6 +404,34 @@ export function CreateBulkActionInspector({
/>
</RadioGroup>
</InputGroup>
{showRegion && (
<InputGroup>
<Label htmlFor="region">Override region</Label>
{/* Our Select primitive uses Ariakit, which treats value={undefined}
as uncontrolled and keeps stale state when switching environments.
The key forces a remount so it reinitializes with the default value. */}
<Select
key={`bulk-region-${environment.id}`}
name="region"
variant="tertiary/medium"
dropdownIcon
items={regionItems}
defaultValue={REPLAY_REGION_NO_OVERRIDE_VALUE}
text={(value) => regionItems.find((r) => r.value === value)?.label}
>
{regionItems.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}
{r.isDefault ? " (default)" : ""}
</SelectItem>
))}
</Select>
<Hint>
By default each run is replayed in its original region. Select a region to run
them all there instead.
</Hint>
</InputGroup>
)}
<InputGroup>
<Label>Preview</Label>
<BulkActionFilterSummary
Expand Down
13 changes: 12 additions & 1 deletion apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export class BulkActionService extends BaseService {
) {
const filters = await getFilters(payload, request);

// Region is a replay-only override that re-routes the replayed runs. It's
// stored alongside the run-list filters under a dedicated key so it isn't
// mistaken for a `regions` selection filter when the params are parsed.
const replayRegion = payload.action === "replay" ? payload.region : undefined;
const params = replayRegion ? { ...filters, replayRegion } : filters;

// Count the runs that will be affected by the bulk action
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(organizationId, "standard");
const runsRepository = new RunsRepository({
Expand All @@ -61,7 +67,7 @@ export class BulkActionService extends BaseService {
userId,
name: payload.title,
type: payload.action === "cancel" ? BulkActionType.CANCEL : BulkActionType.REPLAY,
params: filters,
params,
queryName: "bulk_action_v1",
totalCount: count,
completionNotification:
Expand Down Expand Up @@ -141,6 +147,10 @@ export class BulkActionService extends BaseService {
// 2. Parse the params
const rawParams = group.params && typeof group.params === "object" ? group.params : {};
const finalizeRun = "finalizeRun" in rawParams && (rawParams as any).finalizeRun === true;
const replayRegion =
"replayRegion" in rawParams && typeof (rawParams as any).replayRegion === "string"
? (rawParams as any).replayRegion
: undefined;
const filters = parseRunListInputOptions({
organizationId: group.project.organizationId,
projectId: group.projectId,
Expand Down Expand Up @@ -254,6 +264,7 @@ export class BulkActionService extends BaseService {
replayService.call(run, {
bulkActionId: bulkActionId,
triggerSource: "dashboard",
region: replayRegion,

@devin-ai-integration devin-ai-integration Bot Jun 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Region value uses workerInstanceGroup.name — verify consistency with TriggerTaskService

The region Select sends r.name (the workerInstanceGroup.name field) as the override value. In replayTaskRun.server.ts:71, the fallback is baseWorkerQueue(existingTaskRun.workerQueue) which extracts the base queue name. Both values need to be valid workerInstanceGroup.masterQueue names for region routing to work correctly in TriggerTaskService. This is the same pattern used in the existing single-run replay route (resources.taskruns.$runParam.replay.ts) and the test page, so it should be fine, but worth noting that correctness depends on name and masterQueue being consistent identifiers for region routing.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

})
);
if (error) {
Expand Down
Loading