Skip to content

Commit 3882eae

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/waitpoint-store-coordinator-tri-13440
2 parents 55dd5df + f98e303 commit 3882eae

23 files changed

Lines changed: 3362 additions & 136 deletions
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { derivedFlagsClearedWith } from "~/v3/featureFlags";
2+
3+
export type FlagChange =
4+
| { key: string; type: "added"; newVal: string }
5+
| { key: string; type: "removed"; oldVal: string }
6+
| { key: string; type: "changed"; oldVal: string; newVal: string };
7+
8+
/**
9+
* What a global flag save will do, for the confirm dialog.
10+
*
11+
* A graced primary that is unset also clears its stamps. Those keys are locked, so the caller
12+
* filters them out of `initialValues` — the cascade therefore reads `storedValues`, which is the
13+
* unfiltered set the loader returned. Reading `initialValues` finds nothing and understates the
14+
* deletion, which is the defect this parameter exists to prevent.
15+
*/
16+
export function buildFlagChangeList(params: {
17+
editableKeys: readonly string[];
18+
lockedKeys: readonly string[];
19+
initialValues: Record<string, unknown>;
20+
storedValues: Record<string, unknown>;
21+
newValues: Record<string, unknown>;
22+
}): FlagChange[] {
23+
const { editableKeys, initialValues, storedValues, newValues } = params;
24+
25+
return editableKeys.flatMap<FlagChange>((key) => {
26+
const wasSet = key in initialValues;
27+
const isSet = key in newValues;
28+
const oldVal = initialValues[key];
29+
const newVal = newValues[key];
30+
31+
if (!wasSet && !isSet) return [];
32+
if (wasSet && isSet && stableValue(oldVal) === stableValue(newVal)) return [];
33+
34+
if (!wasSet && isSet) {
35+
return [{ key, type: "added", newVal: String(newVal) }];
36+
}
37+
38+
if (wasSet && !isSet) {
39+
// Only an unset clears the stamps. A change re-stamps instead.
40+
const cascaded = derivedFlagsClearedWith(key)
41+
.filter((derived) => derived in storedValues)
42+
.map<FlagChange>((derived) => ({
43+
key: derived,
44+
type: "removed",
45+
oldVal: String(storedValues[derived]),
46+
}));
47+
return [{ key, type: "removed", oldVal: String(oldVal) }, ...cascaded];
48+
}
49+
50+
return [{ key, type: "changed", oldVal: String(oldVal), newVal: String(newVal) }];
51+
});
52+
}
53+
54+
function stableValue(value: unknown): string {
55+
return JSON.stringify(value ?? null);
56+
}

apps/webapp/app/routes/admin.api.v1.feature-flags.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { json } from "@remix-run/server-runtime";
33
import { prisma } from "~/db.server";
44
import { env } from "~/env.server";
55
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
6-
import { applyGlobalMintKindFlip, makeSetMultipleFlags } from "~/v3/featureFlags.server";
6+
import {
7+
applyGlobalGracedFlips,
8+
makeSetMultipleFlags,
9+
touchesGracedGroup,
10+
withoutDerivedKeys,
11+
} from "~/v3/featureFlags.server";
712
import { validatePartialFeatureFlags } from "~/v3/featureFlags";
813

914
export async function action({ request }: ActionFunctionArgs) {
@@ -25,19 +30,16 @@ export async function action({ request }: ActionFunctionArgs) {
2530
);
2631
}
2732

28-
// Derived grace-stamp fields are computed server-side; never trust them from the body.
29-
const {
30-
runOpsMintKindPrev: _ignoredPrev,
31-
runOpsMintKindFlippedAt: _ignoredFlippedAt,
32-
...requestedFlags
33-
} = validationResult.data;
33+
// Both the strip and the branch derive from the graced-group table, so adding a group needs
34+
// no edit here. Naming the keys inline is how a new group ends up writing its stamp straight
35+
// from the request body, with no lock.
36+
const requestedFlags = withoutDerivedKeys(validationResult.data) as Partial<
37+
typeof validationResult.data
38+
>;
3439

35-
// A global mint-kind flip stamps its grace window under a lock (applyGlobalMintKindFlip);
36-
// any other flag save writes directly.
37-
const updatedFlags =
38-
requestedFlags.runOpsMintKind !== undefined
39-
? await applyGlobalMintKindFlip(prisma, requestedFlags, env.RUN_OPS_MINT_FLIP_GRACE_MS)
40-
: await makeSetMultipleFlags(prisma)(requestedFlags);
40+
const updatedFlags = touchesGracedGroup(requestedFlags)
41+
? await applyGlobalGracedFlips(prisma, requestedFlags, env.RUN_OPS_MINT_FLIP_GRACE_MS)
42+
: await makeSetMultipleFlags(prisma)(requestedFlags);
4143

4244
return json({
4345
success: true,

apps/webapp/app/routes/admin.feature-flags.tsx

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type FeatureFlagKey,
1515
type FlagControlType,
1616
getAllFlagControlTypes,
17+
lockedFlagsInPayload,
1718
validatePartialFeatureFlags,
1819
} from "~/v3/featureFlags";
1920
import { flags as getGlobalFlags, replaceGlobalFeatureFlags } from "~/v3/featureFlags.server";
@@ -29,6 +30,7 @@ import {
2930
DialogFooter,
3031
} from "~/components/primitives/Dialog";
3132
import { cn } from "~/utils/cn";
33+
import { buildFlagChangeList } from "~/components/admin/flagChangeList";
3234
import {
3335
UNSET_VALUE,
3436
BooleanControl,
@@ -111,17 +113,12 @@ export const action = dashboardAction(
111113

112114
const { isManagedCloud } = featuresForRequest(request);
113115

114-
// On managed cloud, reject if payload includes locked flags
115-
if (isManagedCloud) {
116-
const lockedInPayload = Object.keys(parsed.data.flags).filter((key) =>
117-
GLOBAL_LOCKED_FLAGS.includes(key)
116+
const lockedInPayload = lockedFlagsInPayload(Object.keys(parsed.data.flags), isManagedCloud);
117+
if (lockedInPayload.length > 0) {
118+
return json(
119+
{ error: `Cannot modify locked flags: ${lockedInPayload.join(", ")}` },
120+
{ status: 400 }
118121
);
119-
if (lockedInPayload.length > 0) {
120-
return json(
121-
{ error: `Cannot modify locked flags: ${lockedInPayload.join(", ")}` },
122-
{ status: 400 }
123-
);
124-
}
125122
}
126123

127124
const validationResult = validatePartialFeatureFlags(parsed.data.flags);
@@ -137,6 +134,7 @@ export const action = dashboardAction(
137134
catalogKeys: Object.keys(getAllFlagControlTypes()) as FeatureFlagKey[],
138135
isManagedCloud,
139136
unlockLockedFlags: parsed.data.unlockLockedFlags ?? false,
137+
graceMs: env.RUN_OPS_MINT_FLIP_GRACE_MS,
140138
});
141139

142140
return json({ success: true });
@@ -401,6 +399,7 @@ export default function AdminFeatureFlagsRoute() {
401399
open={confirmOpen}
402400
onOpenChange={setConfirmOpen}
403401
initialValues={initialValues}
402+
storedValues={allFlags}
404403
newValues={values}
405404
controlTypes={typedControlTypes}
406405
lockedKeys={unlocked ? [] : GLOBAL_LOCKED_FLAGS}
@@ -467,6 +466,7 @@ function ConfirmDialog({
467466
open,
468467
onOpenChange,
469468
initialValues,
469+
storedValues,
470470
newValues,
471471
controlTypes,
472472
lockedKeys,
@@ -477,6 +477,7 @@ function ConfirmDialog({
477477
open: boolean;
478478
onOpenChange: (open: boolean) => void;
479479
initialValues: Record<string, unknown>;
480+
storedValues: Record<string, unknown>;
480481
newValues: Record<string, unknown>;
481482
controlTypes: Record<string, FlagControlType>;
482483
lockedKeys: readonly string[];
@@ -488,34 +489,12 @@ function ConfirmDialog({
488489
.filter((key) => !lockedKeys.includes(key))
489490
.sort();
490491

491-
type Change =
492-
| { key: string; type: "added"; newVal: string }
493-
| { key: string; type: "removed"; oldVal: string }
494-
| { key: string; type: "changed"; oldVal: string; newVal: string };
495-
496-
const changes = editableKeys.flatMap<Change>((key) => {
497-
const wasSet = key in initialValues;
498-
const isSet = key in newValues;
499-
const oldVal = initialValues[key];
500-
const newVal = newValues[key];
501-
502-
if (!wasSet && !isSet) return [];
503-
if (wasSet && isSet && stableStringify(oldVal) === stableStringify(newVal)) return [];
504-
505-
if (!wasSet && isSet) {
506-
return [{ key, type: "added" as const, newVal: String(newVal) }];
507-
}
508-
if (wasSet && !isSet) {
509-
return [{ key, type: "removed" as const, oldVal: String(oldVal) }];
510-
}
511-
return [
512-
{
513-
key,
514-
type: "changed" as const,
515-
oldVal: String(oldVal),
516-
newVal: String(newVal),
517-
},
518-
];
492+
const changes = buildFlagChangeList({
493+
editableKeys,
494+
lockedKeys,
495+
initialValues,
496+
storedValues,
497+
newValues,
519498
});
520499

521500
return (

0 commit comments

Comments
 (0)