diff --git a/packages/cli/src/deploy-rollout-metadata.ts b/packages/cli/src/deploy-rollout-metadata.ts index 01d3054ff..65d5a7861 100644 --- a/packages/cli/src/deploy-rollout-metadata.ts +++ b/packages/cli/src/deploy-rollout-metadata.ts @@ -5,11 +5,31 @@ export const DeployRolloutMetadataSchema = z source: z.enum(['managed']).optional(), channel: z.enum(['edge', 'stable', 'commit']).optional(), rollout_org_ids: z.array(z.string().min(1)).optional(), + rollout_id: z + .string() + .min(1) + .regex(/^[A-Za-z0-9._:-]+$/) + .optional(), }) - .strict(); + .passthrough(); export type DeployRolloutMetadata = z.infer; +function hasDeployRolloutMetadata(value: DeployRolloutMetadata): boolean { + return Object.entries(value).some(([, fieldValue]) => { + if (fieldValue === undefined) { + return false; + } + if (Array.isArray(fieldValue)) { + return fieldValue.length > 0; + } + if (typeof fieldValue === 'string') { + return fieldValue.length > 0; + } + return true; + }); +} + export function parseDeployRolloutMetadata( raw: string | undefined ): DeployRolloutMetadata | undefined { @@ -30,7 +50,7 @@ export function parseDeployRolloutMetadata( const details = result.error.issues.map((issue) => issue.message).join(', '); throw new Error(`Invalid deploy metadata: ${details}`); } - if (!result.data.source && !result.data.channel && !result.data.rollout_org_ids?.length) { + if (!hasDeployRolloutMetadata(result.data)) { return undefined; } return result.data; @@ -49,18 +69,11 @@ export function mergeDeployRolloutMetadata { + test('accepts rollout_id for Obs-triggered managed deploys', () => { + expect( + parseDeployRolloutMetadata( + '{"source":"managed","channel":"edge","rollout_id":"rollout_3FdsBqGeMpUSDtpBt1uz2n6Drps"}' + ) + ).toEqual({ + source: 'managed', + channel: 'edge', + rollout_id: 'rollout_3FdsBqGeMpUSDtpBt1uz2n6Drps', + }); + }); + + test('passes through unknown keys for forward-compatible managed metadata', () => { + expect( + parseDeployRolloutMetadata( + '{"source":"managed","channel":"edge","rollout_id":"rollout_test","initiated_by":"user_abc"}' + ) + ).toEqual({ + source: 'managed', + channel: 'edge', + rollout_id: 'rollout_test', + initiated_by: 'user_abc', + }); + }); + + test('rejects invalid known fields', () => { + expect(() => parseDeployRolloutMetadata('{"source":"github","channel":"edge"}')).toThrow( + 'Invalid deploy metadata' + ); + }); +}); + +describe('mergeDeployRolloutMetadata', () => { + test('merges rollout metadata onto deployment', () => { + expect( + mergeDeployRolloutMetadata( + { + deployment: { + source: 'managed', + channel: 'edge', + }, + }, + { + source: 'managed', + channel: 'edge', + rollout_id: 'rollout_test', + initiated_by: 'user_abc', + } + ) + ).toEqual({ + deployment: { + source: 'managed', + channel: 'edge', + rollout_id: 'rollout_test', + initiated_by: 'user_abc', + }, + }); + }); +}); diff --git a/packages/server/src/api/project/deploy.ts b/packages/server/src/api/project/deploy.ts index ca27b51c9..e8b24078f 100644 --- a/packages/server/src/api/project/deploy.ts +++ b/packages/server/src/api/project/deploy.ts @@ -215,6 +215,11 @@ export const BuildMetadataSchema = z.object({ source: z.enum(['github', 'cli', 'managed']).optional(), channel: z.enum(['edge', 'stable', 'commit']).optional(), rollout_org_ids: z.array(z.string()).optional(), + rollout_id: z + .string() + .min(1) + .optional() + .describe('correlation id for a managed shared-source rollout wave'), }) ), launch: LaunchMetadataSchema.optional().describe(