diff --git a/packages/sdk/src/cli/commands/apply/apply.test.ts b/packages/sdk/src/cli/commands/apply/apply.test.ts index 8f4bcb6e6..69df1c882 100644 --- a/packages/sdk/src/cli/commands/apply/apply.test.ts +++ b/packages/sdk/src/cli/commands/apply/apply.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "vitest"; import { summarizePlanResultsForDisplay } from "./apply"; +import { formatAuthHookChangeEntries } from "./auth"; +import { buildPlannedExecutorsByName, formatExecutorChangeEntries } from "./executor"; +import { formatResolverChangeEntries } from "./resolver"; +import { formatTailorDBResourceChangeEntries } from "./tailordb"; +import { formatWorkflowChangeEntries } from "./workflow"; type SummaryPlanResults = Parameters[0]; type FunctionRegistryChangeSet = SummaryPlanResults["functionRegistry"]["changeSet"]; @@ -63,6 +68,32 @@ function createFixtureChangeSet< } as unknown as T; } +function computeDisplayEntries(results: SummaryPlanResults) { + return { + executorEntries: formatExecutorChangeEntries( + results.executor.changeSet, + buildPlannedExecutorsByName(results.executor.changeSet), + results.functionRegistry.executorFunctionChanges, + ), + resolverEntries: formatResolverChangeEntries( + results.pipeline.changeSet.resolver, + results.functionRegistry.resolverFunctionChanges, + ), + workflowEntries: formatWorkflowChangeEntries( + results.workflow.changeSet, + results.functionRegistry.workflowJobChanges, + ), + authHookEntries: formatAuthHookChangeEntries( + results.auth.changeSet.authHook, + results.functionRegistry.authHookFunctionChanges, + ), + tailorDBEntries: formatTailorDBResourceChangeEntries( + results.tailorDB.changeSet.type, + results.tailorDB.changeSet.gqlPermission, + ), + }; +} + describe("summarizePlanResultsForDisplay", () => { test("counts grouped display entries instead of raw internal resources", () => { const functionRegistry = createFixtureChangeSet(); @@ -240,7 +271,16 @@ describe("summarizePlanResultsForDisplay", () => { }, } satisfies SummaryPlanResults; - const summary = summarizePlanResultsForDisplay(results); + const { executorEntries, resolverEntries, workflowEntries, authHookEntries, tailorDBEntries } = + computeDisplayEntries(results); + const summary = summarizePlanResultsForDisplay( + results, + executorEntries, + resolverEntries, + workflowEntries, + authHookEntries, + tailorDBEntries, + ); expect(summary).toEqual({ create: 1, @@ -379,7 +419,16 @@ describe("summarizePlanResultsForDisplay", () => { }, } satisfies SummaryPlanResults; - const summary = summarizePlanResultsForDisplay(results); + const { executorEntries, resolverEntries, workflowEntries, authHookEntries, tailorDBEntries } = + computeDisplayEntries(results); + const summary = summarizePlanResultsForDisplay( + results, + executorEntries, + resolverEntries, + workflowEntries, + authHookEntries, + tailorDBEntries, + ); expect(summary).toEqual({ create: 0, @@ -528,7 +577,16 @@ describe("summarizePlanResultsForDisplay", () => { }, } satisfies SummaryPlanResults; - const summary = summarizePlanResultsForDisplay(results); + const { executorEntries, resolverEntries, workflowEntries, authHookEntries, tailorDBEntries } = + computeDisplayEntries(results); + const summary = summarizePlanResultsForDisplay( + results, + executorEntries, + resolverEntries, + workflowEntries, + authHookEntries, + tailorDBEntries, + ); expect(summary).toEqual({ create: 0, diff --git a/packages/sdk/src/cli/commands/apply/apply.ts b/packages/sdk/src/cli/commands/apply/apply.ts index 26ddc531d..2509a5723 100644 --- a/packages/sdk/src/cli/commands/apply/apply.ts +++ b/packages/sdk/src/cli/commands/apply/apply.ts @@ -40,9 +40,15 @@ import { applyFunctionRegistry, collectFunctionEntries, filterBundledWorkflowJobs, + isWorkflowJobFunctionName, planFunctionRegistry, splitFunctionRegistryChanges, } from "./function-registry"; +import { + formatChangeSetEntries, + printGroupedDisplaySection, + type GroupedDisplayEntry, +} from "./grouped-display"; import { applyIdP, planIdP } from "./idp"; import { buildMetaRequest, hasMatchingSdkVersion, sdkNameLabelKey } from "./label"; import { applyPipeline, formatResolverChangeEntries, planPipeline } from "./resolver"; @@ -50,7 +56,6 @@ import { applySecretManager, planSecretManager } from "./secret-manager"; import { applyStaticWebsite, planStaticWebsite } from "./staticwebsite"; import { applyTailorDB, formatTailorDBResourceChangeEntries, planTailorDB } from "./tailordb"; import { applyWorkflow, formatWorkflowChangeEntries, planWorkflow } from "./workflow"; -import type { GroupedDisplayEntry } from "./grouped-display"; import type { OperatorClient } from "@/cli/shared/client"; import type { LoadedConfig } from "@/cli/shared/config-loader"; @@ -182,7 +187,7 @@ async function shouldForceApplyAll( return false; } -function printPlanSummary(results: { +function printPlanResults(results: { functionRegistry: Awaited>; tailorDB: Awaited>; staticWebsite: Awaited>; @@ -194,7 +199,55 @@ function printPlanSummary(results: { workflow: Awaited>; secretManager: Awaited>; }) { - const summary = summarizePlanResultsForDisplay(results); + const executorEntries = formatExecutorChangeEntries( + results.executor.changeSet, + buildPlannedExecutorsByName(results.executor.changeSet), + results.functionRegistry.executorFunctionChanges, + ); + const resolverEntries = formatResolverChangeEntries( + results.pipeline.changeSet.resolver, + results.functionRegistry.resolverFunctionChanges, + ); + const workflowEntries = formatWorkflowChangeEntries( + results.workflow.changeSet, + results.functionRegistry.workflowJobChanges, + ); + const authHookEntries = formatAuthHookChangeEntries( + results.auth.changeSet.authHook, + results.functionRegistry.authHookFunctionChanges, + ); + const tailorDBEntries = formatTailorDBResourceChangeEntries( + results.tailorDB.changeSet.type, + results.tailorDB.changeSet.gqlPermission, + ); + const authEntries: GroupedDisplayEntry[] = [ + ...formatChangeSetEntries(results.auth.changeSet.service, ["service"]), + ...formatChangeSetEntries(results.auth.changeSet.idpConfig, ["idpConfig"]), + ...formatChangeSetEntries(results.auth.changeSet.userProfileConfig, ["userProfileConfig"]), + ...formatChangeSetEntries(results.auth.changeSet.tenantConfig, ["tenantConfig"]), + ...formatChangeSetEntries(results.auth.changeSet.machineUser, ["machineUser"]), + ...authHookEntries, + ...formatChangeSetEntries(results.auth.changeSet.oauth2Client, ["oauth2Client"]), + ...formatChangeSetEntries(results.auth.changeSet.scim, ["scimConfig"]), + ...formatChangeSetEntries(results.auth.changeSet.scimResource, ["scimResource"]), + ]; + + // Print grouped sections + printGroupedDisplaySection("Executors", executorEntries); + printGroupedDisplaySection("Workflows", workflowEntries); + printGroupedDisplaySection("TailorDB resources", tailorDBEntries); + printGroupedDisplaySection("Pipeline resolvers", resolverEntries); + printGroupedDisplaySection("Auth", authEntries); + + // Compute summary + const summary = summarizePlanResultsForDisplay( + results, + executorEntries, + resolverEntries, + workflowEntries, + authHookEntries, + tailorDBEntries, + ); logger.log(formatPlanSummary(summary)); } @@ -299,20 +352,32 @@ function countUnchangedItemsWithoutChangedRelations( * @param results.executor - Planned executor changes * @param results.workflow - Planned workflow changes * @param results.secretManager - Planned secret manager changes + * @param executorEntries - Pre-computed executor display entries + * @param resolverEntries - Pre-computed resolver display entries + * @param workflowEntries - Pre-computed workflow display entries + * @param authHookEntries - Pre-computed auth hook display entries + * @param tailorDBEntries - Pre-computed TailorDB display entries * @returns Aggregated plan summary aligned with grouped display rows */ -export function summarizePlanResultsForDisplay(results: { - functionRegistry: Awaited>; - tailorDB: Awaited>; - staticWebsite: Awaited>; - idp: Awaited>; - auth: Awaited>; - pipeline: Awaited>; - app: Awaited>; - executor: Awaited>; - workflow: Awaited>; - secretManager: Awaited>; -}): PlanSummary { +export function summarizePlanResultsForDisplay( + results: { + functionRegistry: Awaited>; + tailorDB: Awaited>; + staticWebsite: Awaited>; + idp: Awaited>; + auth: Awaited>; + pipeline: Awaited>; + app: Awaited>; + executor: Awaited>; + workflow: Awaited>; + secretManager: Awaited>; + }, + executorEntries: ReadonlyArray, + resolverEntries: ReadonlyArray, + workflowEntries: ReadonlyArray, + authHookEntries: ReadonlyArray, + tailorDBEntries: ReadonlyArray, +): PlanSummary { const summary: PlanSummary = { create: 0, update: 0, @@ -348,10 +413,7 @@ export function summarizePlanResultsForDisplay(results: { addPlanSummary( summary, summarizeDisplayEntries( - formatTailorDBResourceChangeEntries( - results.tailorDB.changeSet.type, - results.tailorDB.changeSet.gqlPermission, - ), + tailorDBEntries, countUnchangedNamesExcludingChanged( [ results.tailorDB.changeSet.type.unchanged, @@ -374,10 +436,7 @@ export function summarizePlanResultsForDisplay(results: { addPlanSummary( summary, summarizeDisplayEntries( - formatResolverChangeEntries( - results.pipeline.changeSet.resolver, - results.functionRegistry.resolverFunctionChanges, - ), + resolverEntries, countUnchangedItemsWithoutChangedRelations( results.pipeline.changeSet.resolver.unchanged as ReadonlyArray< HasName & { namespaceName?: string } @@ -397,11 +456,7 @@ export function summarizePlanResultsForDisplay(results: { addPlanSummary( summary, summarizeDisplayEntries( - formatExecutorChangeEntries( - results.executor.changeSet, - buildPlannedExecutorsByName(results.executor.changeSet), - results.functionRegistry.executorFunctionChanges, - ), + executorEntries, countUnchangedItemsWithoutChangedRelations( results.executor.changeSet.unchanged, [ @@ -418,10 +473,7 @@ export function summarizePlanResultsForDisplay(results: { addPlanSummary( summary, summarizeDisplayEntries( - formatWorkflowChangeEntries( - results.workflow.changeSet, - results.functionRegistry.workflowJobChanges, - ), + workflowEntries, countUnchangedItemsWithoutChangedRelations( results.workflow.changeSet.unchanged as ReadonlyArray< HasName & { usedJobNames?: string[] } @@ -440,10 +492,7 @@ export function summarizePlanResultsForDisplay(results: { addPlanSummary( summary, summarizeDisplayEntries( - formatAuthHookChangeEntries( - results.auth.changeSet.authHook, - results.functionRegistry.authHookFunctionChanges, - ), + authHookEntries, countUnchangedItemsWithoutChangedRelations( results.auth.changeSet.authHook.unchanged, [ @@ -627,23 +676,18 @@ export async function apply(options?: ApplyOptions) { ); const unchangedWorkflowJobs = new Set( functionRegistry.changeSet.unchanged - .map((entry) => entry.name) - .filter((name) => name.startsWith("workflow--")) - .map((name) => name.slice("workflow--".length)), + .filter((entry) => isWorkflowJobFunctionName(entry.name)) + .map((entry) => entry.name.slice("workflow--".length)), ); const [tailorDB, staticWebsite, idp, auth, pipeline, app, executor, workflow, secretManager] = await Promise.all([ withSpan("plan.tailorDB", () => planTailorDB(ctx)), withSpan("plan.staticWebsite", () => planStaticWebsite(ctx)), withSpan("plan.idp", () => planIdP(ctx)), - withSpan("plan.auth", () => planAuth(ctx, functionRegistry.authHookFunctionChanges)), - withSpan("plan.pipeline", () => - planPipeline(ctx, functionRegistry.resolverFunctionChanges), - ), + withSpan("plan.auth", () => planAuth(ctx)), + withSpan("plan.pipeline", () => planPipeline(ctx)), withSpan("plan.application", () => planApplication(ctx)), - withSpan("plan.executor", () => - planExecutor(ctx, functionRegistry.executorFunctionChanges), - ), + withSpan("plan.executor", () => planExecutor(ctx)), withSpan("plan.workflow", () => planWorkflow( client, @@ -652,7 +696,6 @@ export async function apply(options?: ApplyOptions) { workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs, - functionRegistry.workflowJobChanges, ), ), withSpan("plan.secretManager", () => planSecretManager(ctx)), @@ -763,7 +806,7 @@ export async function apply(options?: ApplyOptions) { } }); - printPlanSummary({ + printPlanResults({ functionRegistry, tailorDB, staticWebsite, diff --git a/packages/sdk/src/cli/commands/apply/auth.plan.test.ts b/packages/sdk/src/cli/commands/apply/auth.plan.test.ts index 328c7377c..3d005e38e 100644 --- a/packages/sdk/src/cli/commands/apply/auth.plan.test.ts +++ b/packages/sdk/src/cli/commands/apply/auth.plan.test.ts @@ -6,7 +6,6 @@ import { AuthOAuth2Client_GrantType, } from "@tailor-proto/tailor/v1/auth_resource_pb"; import { describe, expect, test, vi } from "vitest"; -import { logger } from "@/cli/shared/logger"; import { formatAuthHookChangeEntries, planAuth } from "./auth"; import type { PlanContext } from "./apply"; import type { Application } from "@/cli/services/application"; @@ -480,21 +479,6 @@ describe("planAuth", () => { expect(result.changeSet.idpConfig.updates[0]?.name).toBe("default"); expect(result.changeSet.idpConfig.unchanged).toHaveLength(0); }); - - test("prints auth child resources under a single flat Auth section", async () => { - const client = createMockClient(); - const logSpy = vi.spyOn(logger, "log").mockImplementation(() => {}); - - await planAuth(createContext(client)); - - expect(logSpy.mock.calls.map(([message]) => message)).toEqual([ - "Auth:", - " + auth-a (service)", - " + manager-machine-user (machineUser)", - " + auth-a/before-login (authHook)", - " + sample (oauth2Client)", - ]); - }); }); describe("formatAuthHookChangeEntries", () => { diff --git a/packages/sdk/src/cli/commands/apply/auth.ts b/packages/sdk/src/cli/commands/apply/auth.ts index 4c12d9c52..218c6c41a 100644 --- a/packages/sdk/src/cli/commands/apply/auth.ts +++ b/packages/sdk/src/cli/commands/apply/auth.ts @@ -28,14 +28,8 @@ import { createChangeSet, type HasName } from "./change-set"; import { areNormalizedEqual, normalizeProtoConfig, normalizeStringArray } from "./compare"; import { authHookFunctionName } from "./function-registry"; import { - actionSymbol, - buildRemainingFunctionRegistryEntries, - createRelatedFunctionRegistryNameSets, - formatChangeSetEntries, + formatChangeEntriesWithFunctionRegistry, type GroupedDisplayEntry, - printGroupedDisplaySection, - type PrintableChangeSet, - type RelatedFunctionRegistryNameSets, type RelatedFunctionRegistryChanges, } from "./grouped-display"; import { idpClientSecretName, idpClientVaultName } from "./idp"; @@ -264,13 +258,9 @@ export async function applyAuth( /** * Plan auth-related changes based on current and desired state. * @param context - Planning context - * @param functionRegistryAuthHookChanges - Related function registry changes for auth hooks * @returns Planned auth changes and metadata */ -export async function planAuth( - context: PlanContext, - functionRegistryAuthHookChanges?: RelatedFunctionRegistryChanges, -) { +export async function planAuth(context: PlanContext) { const { client, workspaceId, application, forRemoval, forceApplyAll = false } = context; const auths: Readonly[] = []; if (!forRemoval && application.authService) { @@ -304,20 +294,6 @@ export async function planAuth( planSCIMResources(client, workspaceId, auths, deletedServices), ]); - printAuthChanges( - { - service: serviceChangeSet, - idpConfig: idpConfigChangeSet, - userProfileConfig: userProfileConfigChangeSet, - tenantConfig: tenantConfigChangeSet, - machineUser: machineUserChangeSet, - authHook: authHookChangeSet, - oauth2Client: oauth2ClientChangeSet, - scim: scimChangeSet, - scimResource: scimResourceChangeSet, - }, - functionRegistryAuthHookChanges, - ); return { changeSet: { service: serviceChangeSet, @@ -336,39 +312,6 @@ export async function planAuth( }; } -function printAuthChanges( - changeSet: { - service: PrintableChangeSet; - idpConfig: PrintableChangeSet; - userProfileConfig: PrintableChangeSet; - tenantConfig: PrintableChangeSet; - machineUser: PrintableChangeSet; - authHook: AuthHookChangeSet; - oauth2Client: PrintableChangeSet; - scim: PrintableChangeSet; - scimResource: PrintableChangeSet; - }, - functionRegistryAuthHookChanges?: RelatedFunctionRegistryChanges, -) { - const authHookEntries = formatAuthHookChangeEntries( - changeSet.authHook, - functionRegistryAuthHookChanges, - ); - const entries: GroupedDisplayEntry[] = [ - ...formatChangeSetEntries(changeSet.service, ["service"]), - ...formatChangeSetEntries(changeSet.idpConfig, ["idpConfig"]), - ...formatChangeSetEntries(changeSet.userProfileConfig, ["userProfileConfig"]), - ...formatChangeSetEntries(changeSet.tenantConfig, ["tenantConfig"]), - ...formatChangeSetEntries(changeSet.machineUser, ["machineUser"]), - ...authHookEntries, - ...formatChangeSetEntries(changeSet.oauth2Client, ["oauth2Client"]), - ...formatChangeSetEntries(changeSet.scim, ["scimConfig"]), - ...formatChangeSetEntries(changeSet.scimResource, ["scimResource"]), - ]; - - printGroupedDisplaySection("Auth", entries); -} - type CreateService = { name: string; request: MessageInitShape; @@ -1908,10 +1851,6 @@ type DeleteAuthHook = { request: MessageInitShape; }; -type AuthHookChangeSet = ReturnType< - typeof createChangeSet ->; - function areAuthHooksEqual( existing: { scriptRef?: string; @@ -1950,13 +1889,6 @@ function areAuthHooksEqual( ); } -type AuthHookDisplayEntry = GroupedDisplayEntry; - -function authHookFunctionNameFromDisplayName(name: string) { - const [namespace, hookPoint] = name.split("/"); - return namespace && hookPoint ? authHookFunctionName(namespace, hookPoint) : undefined; -} - /** * Format auth hook changes for grouped dry-run display. * @param changeSet - Auth hook changes @@ -1965,10 +1897,6 @@ function authHookFunctionNameFromDisplayName(name: string) { * @param changeSet.deletes - Auth hook deletions * @param changeSet.replaces - Auth hook replacements * @param functionRegistryAuthHookChanges - Related function registry changes for auth hooks - * @param functionRegistryAuthHookChanges.creates - Function registry creations - * @param functionRegistryAuthHookChanges.updates - Function registry updates - * @param functionRegistryAuthHookChanges.deletes - Function registry deletions - * @param functionRegistryAuthHookChanges.replaces - Function registry replacements * @returns Display entries for auth hook output */ export function formatAuthHookChangeEntries( @@ -1979,69 +1907,16 @@ export function formatAuthHookChangeEntries( replaces: ReadonlyArray; }, functionRegistryAuthHookChanges?: RelatedFunctionRegistryChanges, -): AuthHookDisplayEntry[] { - const functionNames = createRelatedFunctionRegistryNameSets(functionRegistryAuthHookChanges); - const consumed: RelatedFunctionRegistryNameSets = createRelatedFunctionRegistryNameSets(); - - const createEntries = changeSet.creates.map((item) => { - const functionName = authHookFunctionNameFromDisplayName(item.name); - const hasFunctionRegistryChange = Boolean( - functionName && functionNames.creates.has(functionName), - ); - if (functionName && hasFunctionRegistryChange) { - consumed.creates.add(functionName); - } - return { - action: "create" as const, - symbol: actionSymbol("create"), - name: item.name, - labels: hasFunctionRegistryChange ? ["authHook", "functionRegistry"] : ["authHook"], - }; - }); - const deleteEntries = changeSet.deletes.map((item) => { - const functionName = authHookFunctionNameFromDisplayName(item.name); - const hasFunctionRegistryChange = Boolean( - functionName && functionNames.deletes.has(functionName), - ); - if (functionName && hasFunctionRegistryChange) { - consumed.deletes.add(functionName); - } - return { - action: "delete" as const, - symbol: actionSymbol("delete"), - name: item.name, - labels: hasFunctionRegistryChange ? ["authHook", "functionRegistry"] : ["authHook"], - }; - }); - const updateEntries = changeSet.updates.map((item) => { - const functionName = authHookFunctionNameFromDisplayName(item.name); - const hasFunctionRegistryChange = Boolean( - functionName && functionNames.updates.has(functionName), - ); - if (functionName && hasFunctionRegistryChange) { - consumed.updates.add(functionName); - } - return { - action: "update" as const, - symbol: actionSymbol("update"), - name: item.name, - labels: hasFunctionRegistryChange ? ["authHook", "functionRegistry"] : ["authHook"], - }; - }); - const replaceEntries = changeSet.replaces.map((item) => ({ - action: "replace" as const, - symbol: actionSymbol("replace"), - name: item.name, - labels: ["authHook"], - })); - - return [ - ...createEntries, - ...deleteEntries, - ...updateEntries, - ...replaceEntries, - ...buildRemainingFunctionRegistryEntries(functionNames, consumed), - ]; +): GroupedDisplayEntry[] { + return formatChangeEntriesWithFunctionRegistry( + "authHook", + changeSet, + functionRegistryAuthHookChanges, + (item) => { + const [namespace, hookPoint] = item.name.split("/"); + return namespace && hookPoint ? [authHookFunctionName(namespace, hookPoint)] : []; + }, + ); } async function planAuthHooks( diff --git a/packages/sdk/src/cli/commands/apply/executor.ts b/packages/sdk/src/cli/commands/apply/executor.ts index ffc16184a..f50948ab2 100644 --- a/packages/sdk/src/cli/commands/apply/executor.ts +++ b/packages/sdk/src/cli/commands/apply/executor.ts @@ -15,18 +15,14 @@ import { ExecutorTriggerType, } from "@tailor-proto/tailor/v1/executor_resource_pb"; import { fetchAll, type OperatorClient } from "@/cli/shared/client"; -import { logger, styles } from "@/cli/shared/logger"; import { buildExecutorArgsExpr } from "@/cli/shared/runtime-args"; import { stringifyFunction } from "@/parser/service/tailordb"; -import { createChangeSet, type ChangeSet, type HasName } from "./change-set"; +import { createChangeSet, type ChangeSet } from "./change-set"; import { areNormalizedEqual, normalizeProtoConfig } from "./compare"; import { executorFunctionName } from "./function-registry"; import { - actionSymbol, - buildRemainingFunctionRegistryEntries, - createRelatedFunctionRegistryNameSets, + formatChangeEntriesWithFunctionRegistry, type GroupedDisplayEntry, - type RelatedFunctionRegistryNameSets, type RelatedFunctionRegistryChanges, } from "./grouped-display"; import { buildMetaRequest, hasMatchingSdkVersion, sdkNameLabelKey, type WithLabel } from "./label"; @@ -92,13 +88,9 @@ function trn(workspaceId: string, name: string) { /** * Plan executor-related changes based on current and desired state. * @param context - Planning context - * @param functionRegistryExecutorChanges - Related function registry changes for executors * @returns Planned changes */ -export async function planExecutor( - context: PlanContext, - functionRegistryExecutorChanges?: RelatedFunctionRegistryChanges, -) { +export async function planExecutor(context: PlanContext) { const { client, workspaceId, application, forRemoval } = context; const changeSet = createChangeSet("Executors"); const conflicts: OwnerConflict[] = []; @@ -198,7 +190,6 @@ export async function planExecutor( } }); - printExecutorChanges(changeSet, functionRegistryExecutorChanges); return { changeSet, conflicts, unmanaged, resourceOwners }; } @@ -231,10 +222,6 @@ export function buildPlannedExecutorsByName( * @param changeSet - Executor changes * @param executors - Desired executor configs keyed by name * @param functionRegistryExecutorChanges - Related function registry changes for executors - * @param functionRegistryExecutorChanges.creates - Function registry creations - * @param functionRegistryExecutorChanges.updates - Function registry updates - * @param functionRegistryExecutorChanges.deletes - Function registry deletions - * @param functionRegistryExecutorChanges.replaces - Function registry replacements * @returns Display entries for executor output */ export function formatExecutorChangeEntries( @@ -245,90 +232,20 @@ export function formatExecutorChangeEntries( executors: Record | undefined>, functionRegistryExecutorChanges?: RelatedFunctionRegistryChanges, ): ExecutorDisplayEntry[] { - const functionNames = createRelatedFunctionRegistryNameSets(functionRegistryExecutorChanges); - const consumed: RelatedFunctionRegistryNameSets = createRelatedFunctionRegistryNameSets(); - - const createEntries = changeSet.creates.map((item) => { - const executor = executors[item.name]; - const functionName = executorFunctionName(item.name); - const hasFunctionRegistryChange = - executor && isFunctionBackedExecutor(executor) && functionNames.creates.has(functionName); - if (hasFunctionRegistryChange) { - consumed.creates.add(functionName); - } - return { - action: "create" as const, - symbol: actionSymbol("create"), - name: item.name, - labels: hasFunctionRegistryChange ? ["executor", "functionRegistry"] : ["executor"], - }; - }); - const deleteEntries = changeSet.deletes.map((item) => { - const functionName = executorFunctionName(item.name); - const hasFunctionRegistryChange = functionNames.deletes.has(functionName); - if (hasFunctionRegistryChange) { - consumed.deletes.add(functionName); - } - return { - action: "delete" as const, - symbol: actionSymbol("delete"), - name: item.name, - labels: hasFunctionRegistryChange ? ["executor", "functionRegistry"] : ["executor"], - }; - }); - const updateEntries = changeSet.updates.map((item) => { - const executor = executors[item.name]; - const functionName = executorFunctionName(item.name); - const hasFunctionRegistryChange = - executor && isFunctionBackedExecutor(executor) && functionNames.updates.has(functionName); - if (hasFunctionRegistryChange) { - consumed.updates.add(functionName); - } - return { - action: "update" as const, - symbol: actionSymbol("update"), - name: item.name, - labels: hasFunctionRegistryChange ? ["executor", "functionRegistry"] : ["executor"], - }; - }); - const replaceEntries = (changeSet.replaces as ReadonlyArray).map((item) => ({ - action: "replace" as const, - symbol: actionSymbol("replace"), - name: item.name, - labels: ["executor"], - })); - - return [ - ...createEntries, - ...deleteEntries, - ...updateEntries, - ...replaceEntries, - ...buildRemainingFunctionRegistryEntries(functionNames, consumed), - ]; -} - -function printExecutorChanges( - changeSet: ChangeSet, - functionRegistryExecutorChanges?: { - creates: ReadonlyArray; - updates: ReadonlyArray; - deletes: ReadonlyArray; - replaces: ReadonlyArray; - }, -) { - const entries = formatExecutorChangeEntries( + return formatChangeEntriesWithFunctionRegistry( + "executor", changeSet, - buildPlannedExecutorsByName(changeSet), functionRegistryExecutorChanges, + (item, action) => { + if (action === "delete") { + return [executorFunctionName(item.name)]; + } + const executor = executors[item.name]; + return executor && isFunctionBackedExecutor(executor) + ? [executorFunctionName(item.name)] + : []; + }, ); - if (entries.length === 0) { - return; - } - - logger.log(styles.bold("Executors:")); - for (const entry of entries) { - logger.log(` ${entry.symbol} ${entry.name} (${entry.labels.join(", ")})`); - } } function normalizeComparableExecutor(executor: MessageInitShape) { diff --git a/packages/sdk/src/cli/commands/apply/grouped-display.ts b/packages/sdk/src/cli/commands/apply/grouped-display.ts index 80e2fe9e4..88e9a618a 100644 --- a/packages/sdk/src/cli/commands/apply/grouped-display.ts +++ b/packages/sdk/src/cli/commands/apply/grouped-display.ts @@ -109,30 +109,6 @@ function formatGroupedDisplayLine(entry: GroupedDisplayEntry) { : `${entry.symbol} ${entry.name}`; } -/** - * Print a titled section of grouped display entries. - * @param title - Section title - * @param entries - Entries to print - * @param indent - Leading spaces before the title - * @returns True when any entries were printed - */ -export function printGroupedDisplaySection( - title: string, - entries: ReadonlyArray, - indent = 0, -) { - if (entries.length === 0) { - return false; - } - - logger.log(styles.bold(`${" ".repeat(indent)}${title}:`)); - const entryIndent = " ".repeat(indent + 2); - for (const entry of entries) { - logger.log(`${entryIndent}${formatGroupedDisplayLine(entry)}`); - } - return true; -} - function formatFunctionRegistryDisplayName(name: string): string { if (name.startsWith("resolver--")) { const [, namespace, resolverName] = name.split("--"); @@ -169,38 +145,119 @@ export function buildRemainingFunctionRegistryEntries( names: RelatedFunctionRegistryNameSets, consumed: RelatedFunctionRegistryNameSets = createRelatedFunctionRegistryNameSets(), ): GroupedDisplayEntry[] { - return [ - ...[...names.creates] - .filter((name) => !consumed.creates.has(name)) - .map((name) => ({ - action: "create" as const, - symbol: actionSymbol("create"), - name: formatFunctionRegistryDisplayName(name), - labels: ["functionRegistry"], - })), - ...[...names.deletes] - .filter((name) => !consumed.deletes.has(name)) - .map((name) => ({ - action: "delete" as const, - symbol: actionSymbol("delete"), - name: formatFunctionRegistryDisplayName(name), - labels: ["functionRegistry"], - })), - ...[...names.updates] - .filter((name) => !consumed.updates.has(name)) - .map((name) => ({ - action: "update" as const, - symbol: actionSymbol("update"), - name: formatFunctionRegistryDisplayName(name), - labels: ["functionRegistry"], - })), - ...[...names.replaces] - .filter((name) => !consumed.replaces.has(name)) + const actions = [ + ["create", names.creates, consumed.creates], + ["delete", names.deletes, consumed.deletes], + ["update", names.updates, consumed.updates], + ["replace", names.replaces, consumed.replaces], + ] as const; + + return actions.flatMap(([action, nameSet, consumedSet]) => + [...nameSet] + .filter((name) => !consumedSet.has(name)) .map((name) => ({ - action: "replace" as const, - symbol: actionSymbol("replace"), + action, + symbol: actionSymbol(action), name: formatFunctionRegistryDisplayName(name), labels: ["functionRegistry"], })), + ); +} + +/** + * Format change set entries with function registry grouping. + * + * For each item in creates/updates/deletes, calls `getFunctionRegistryNames` to + * derive zero or more function registry names. When a matching function registry + * change exists for the same action, the item is displayed with both the resource + * label and "functionRegistry". Ungrouped function registry changes are appended. + * @param resourceLabel - Label for the resource kind (e.g. "executor", "resolver") + * @param changeSet - Resource change set with creates/updates/deletes/replaces + * @param changeSet.creates - Created resources + * @param changeSet.updates - Updated resources + * @param changeSet.deletes - Deleted resources + * @param changeSet.replaces - Replaced resources + * @param functionRegistryChanges - Related function registry changes + * @param getFunctionRegistryNames - Derives function registry names from a resource item + * @returns Display entries for CLI output + */ +export function formatChangeEntriesWithFunctionRegistry< + C extends HasName, + U extends HasName, + D extends HasName, +>( + resourceLabel: string, + changeSet: { + creates: ReadonlyArray; + updates: ReadonlyArray; + deletes: ReadonlyArray; + replaces: ReadonlyArray; + }, + functionRegistryChanges: RelatedFunctionRegistryChanges | undefined, + getFunctionRegistryNames: (item: C | U | D, action: DisplayAction) => string[], +): GroupedDisplayEntry[] { + const functionNames = createRelatedFunctionRegistryNameSets(functionRegistryChanges); + const consumed: RelatedFunctionRegistryNameSets = createRelatedFunctionRegistryNameSets(); + + function processItems( + items: ReadonlyArray, + action: DisplayAction, + fnNameSet: Set, + consumedSet: Set, + ): GroupedDisplayEntry[] { + return items.map((item) => { + const names = getFunctionRegistryNames(item, action); + const hasMatch = names.some((name) => fnNameSet.has(name)); + if (hasMatch) { + for (const name of names) { + if (fnNameSet.has(name)) { + consumedSet.add(name); + } + } + } + return { + action, + symbol: actionSymbol(action), + name: item.name, + labels: hasMatch ? [resourceLabel, "functionRegistry"] : [resourceLabel], + }; + }); + } + + return [ + ...processItems(changeSet.creates, "create", functionNames.creates, consumed.creates), + ...processItems(changeSet.deletes, "delete", functionNames.deletes, consumed.deletes), + ...processItems(changeSet.updates, "update", functionNames.updates, consumed.updates), + ...changeSet.replaces.map((item) => ({ + action: "replace" as const, + symbol: actionSymbol("replace"), + name: item.name, + labels: [resourceLabel], + })), + ...buildRemainingFunctionRegistryEntries(functionNames, consumed), ]; } + +/** + * Print a titled section of grouped display entries. + * @param title - Section title + * @param entries - Entries to print + * @param indent - Leading spaces before the title + * @returns True when any entries were printed + */ +export function printGroupedDisplaySection( + title: string, + entries: ReadonlyArray, + indent = 0, +) { + if (entries.length === 0) { + return false; + } + + logger.log(styles.bold(`${" ".repeat(indent)}${title}:`)); + const entryIndent = " ".repeat(indent + 2); + for (const entry of entries) { + logger.log(`${entryIndent}${formatGroupedDisplayLine(entry)}`); + } + return true; +} diff --git a/packages/sdk/src/cli/commands/apply/resolver.ts b/packages/sdk/src/cli/commands/apply/resolver.ts index e05ec8d59..32b4c9d01 100644 --- a/packages/sdk/src/cli/commands/apply/resolver.ts +++ b/packages/sdk/src/cli/commands/apply/resolver.ts @@ -18,17 +18,13 @@ import { import * as inflection from "inflection"; import { type ResolverService } from "@/cli/services/resolver/service"; import { fetchAll, type OperatorClient } from "@/cli/shared/client"; -import { logger, styles } from "@/cli/shared/logger"; import { buildResolverOperationHookExpr } from "@/cli/shared/runtime-args"; import { createChangeSet, type ChangeSet, type HasName } from "./change-set"; import { areNormalizedEqual, normalizeProtoConfig } from "./compare"; import { resolverFunctionName } from "./function-registry"; import { - actionSymbol, - buildRemainingFunctionRegistryEntries, - createRelatedFunctionRegistryNameSets, + formatChangeEntriesWithFunctionRegistry, type GroupedDisplayEntry, - type RelatedFunctionRegistryNameSets, type RelatedFunctionRegistryChanges, } from "./grouped-display"; import { buildMetaRequest, hasMatchingSdkVersion, sdkNameLabelKey, type WithLabel } from "./label"; @@ -102,13 +98,9 @@ export async function applyPipeline( /** * Plan resolver pipeline changes based on current and desired state. * @param context - Planning context - * @param functionRegistryResolverChanges - Related function registry changes for resolvers * @returns Planned changes */ -export async function planPipeline( - context: PlanContext, - functionRegistryResolverChanges?: RelatedFunctionRegistryChanges, -) { +export async function planPipeline(context: PlanContext) { const { client, workspaceId, application, forRemoval, forceApplyAll = false } = context; const pipelines: Readonly[] = []; if (!forRemoval) { @@ -138,8 +130,6 @@ export async function planPipeline( forceApplyAll, ); - serviceChangeSet.print(); - printResolverChanges(resolverChangeSet, functionRegistryResolverChanges); return { changeSet: { service: serviceChangeSet, @@ -427,18 +417,10 @@ async function planResolvers( type ResolverDisplayEntry = GroupedDisplayEntry; -function formatResolverFunctionName(namespace: string | undefined, resolverName: string) { - return namespace ? resolverFunctionName(namespace, resolverName) : undefined; -} - /** * Format resolver changes for grouped dry-run display. * @param changeSet - Resolver changes * @param resolverFunctionChanges - Related function registry changes for resolvers - * @param resolverFunctionChanges.creates - Function registry creations - * @param resolverFunctionChanges.updates - Function registry updates - * @param resolverFunctionChanges.deletes - Function registry deletions - * @param resolverFunctionChanges.replaces - Function registry replacements * @returns Display entries for resolver output */ export function formatResolverChangeEntries( @@ -448,88 +430,15 @@ export function formatResolverChangeEntries( >, resolverFunctionChanges?: RelatedFunctionRegistryChanges, ): ResolverDisplayEntry[] { - const functionNames = createRelatedFunctionRegistryNameSets(resolverFunctionChanges); - const consumed: RelatedFunctionRegistryNameSets = createRelatedFunctionRegistryNameSets(); - - const createEntries = changeSet.creates.map((item) => { - const functionName = formatResolverFunctionName(item.request.namespaceName, item.name); - const hasFunctionRegistryChange = Boolean( - functionName && functionNames.creates.has(functionName), - ); - if (functionName && hasFunctionRegistryChange) { - consumed.creates.add(functionName); - } - return { - action: "create" as const, - symbol: actionSymbol("create"), - name: item.name, - labels: hasFunctionRegistryChange ? ["resolver", "functionRegistry"] : ["resolver"], - }; - }); - const deleteEntries = changeSet.deletes.map((item) => { - const functionName = formatResolverFunctionName(item.request.namespaceName, item.name); - const hasFunctionRegistryChange = Boolean( - functionName && functionNames.deletes.has(functionName), - ); - if (functionName && hasFunctionRegistryChange) { - consumed.deletes.add(functionName); - } - return { - action: "delete" as const, - symbol: actionSymbol("delete"), - name: item.name, - labels: hasFunctionRegistryChange ? ["resolver", "functionRegistry"] : ["resolver"], - }; - }); - const updateEntries = changeSet.updates.map((item) => { - const functionName = formatResolverFunctionName(item.request.namespaceName, item.name); - const hasFunctionRegistryChange = Boolean( - functionName && functionNames.updates.has(functionName), - ); - if (functionName && hasFunctionRegistryChange) { - consumed.updates.add(functionName); - } - return { - action: "update" as const, - symbol: actionSymbol("update"), - name: item.name, - labels: hasFunctionRegistryChange ? ["resolver", "functionRegistry"] : ["resolver"], - }; - }); - const replaceEntries = (changeSet.replaces as ReadonlyArray).map((item) => ({ - action: "replace" as const, - symbol: actionSymbol("replace"), - name: item.name, - labels: ["resolver"], - })); - - return [ - ...createEntries, - ...deleteEntries, - ...updateEntries, - ...replaceEntries, - ...buildRemainingFunctionRegistryEntries(functionNames, consumed), - ]; -} - -function printResolverChanges( - changeSet: ChangeSet, - resolverFunctionChanges?: { - creates: ReadonlyArray; - updates: ReadonlyArray; - deletes: ReadonlyArray; - replaces: ReadonlyArray; - }, -) { - const entries = formatResolverChangeEntries(changeSet, resolverFunctionChanges); - if (entries.length === 0) { - return; - } - - logger.log(styles.bold("Pipeline resolvers:")); - for (const entry of entries) { - logger.log(` ${entry.symbol} ${entry.name} (${entry.labels.join(", ")})`); - } + return formatChangeEntriesWithFunctionRegistry( + "resolver", + changeSet, + resolverFunctionChanges, + (item) => { + const namespace = "request" in item ? item.request.namespaceName : undefined; + return namespace ? [resolverFunctionName(namespace, item.name)] : []; + }, + ); } function normalizeComparableResolver(resolver: MessageInitShape) { diff --git a/packages/sdk/src/cli/commands/apply/tailordb/index.test.ts b/packages/sdk/src/cli/commands/apply/tailordb/index.test.ts index b74bc9109..060e9a58a 100644 --- a/packages/sdk/src/cli/commands/apply/tailordb/index.test.ts +++ b/packages/sdk/src/cli/commands/apply/tailordb/index.test.ts @@ -623,6 +623,38 @@ describe("formatTailorDBResourceChangeEntries", () => { ]); }); + test("shows separate entries when type and gqlPermission have different actions for the same name", () => { + const entries = formatTailorDBResourceChangeEntries( + { + creates: [{ name: "Project" }], + updates: [], + deletes: [], + replaces: [], + }, + { + creates: [], + updates: [{ name: "Project" }], + deletes: [], + replaces: [], + }, + ); + + expect(entries).toEqual([ + { + action: "create", + symbol: "+", + name: "Project", + labels: ["type"], + }, + { + action: "update", + symbol: "~", + name: "Project", + labels: ["gqlPermission"], + }, + ]); + }); + test("keeps standalone gqlPermission changes visible", () => { const entries = formatTailorDBResourceChangeEntries( { diff --git a/packages/sdk/src/cli/commands/apply/tailordb/index.ts b/packages/sdk/src/cli/commands/apply/tailordb/index.ts index 0bb5c2719..f687dce62 100644 --- a/packages/sdk/src/cli/commands/apply/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/apply/tailordb/index.ts @@ -56,7 +56,7 @@ import { } from "@/cli/commands/tailordb/migrate/snapshot"; import { type TailorDBService } from "@/cli/services/tailordb/service"; import { fetchAll, type OperatorClient } from "@/cli/shared/client"; -import { logger, styles } from "@/cli/shared/logger"; +import { logger } from "@/cli/shared/logger"; import { createChangeSet, type HasName, type ChangeSet } from "../change-set"; import { areNormalizedEqual, normalizeProtoConfig } from "../compare"; import { actionSymbol, type DisplayAction, type GroupedDisplayEntry } from "../grouped-display"; @@ -1006,9 +1006,6 @@ export async function planTailorDB(context: PlanContext) { planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll), ]); - serviceChangeSet.print(); - printTailorDBResourceChanges(typeChangeSet, gqlPermissionChangeSet); - return { changeSet: { service: serviceChangeSet, @@ -1094,21 +1091,6 @@ export function formatTailorDBResourceChangeEntries( ]; } -function printTailorDBResourceChanges( - typeChangeSet: ChangeSet, - gqlPermissionChangeSet: ChangeSet, -) { - const entries = formatTailorDBResourceChangeEntries(typeChangeSet, gqlPermissionChangeSet); - if (entries.length === 0) { - return; - } - - logger.log(styles.bold("TailorDB resources:")); - for (const entry of entries) { - logger.log(` ${entry.symbol} ${entry.name} (${entry.labels.join(", ")})`); - } -} - type CreateService = { name: string; request: MessageInitShape; diff --git a/packages/sdk/src/cli/commands/apply/workflow.test.ts b/packages/sdk/src/cli/commands/apply/workflow.test.ts index a324b5985..33964a437 100644 --- a/packages/sdk/src/cli/commands/apply/workflow.test.ts +++ b/packages/sdk/src/cli/commands/apply/workflow.test.ts @@ -108,7 +108,14 @@ describe("planWorkflow", () => { "main-job": ["main-job"], }; - const result = await planWorkflow(client, workspaceId, appName, workflows, mainJobDeps); + const result = await planWorkflow( + client, + workspaceId, + appName, + workflows, + mainJobDeps, + new Set(), + ); // "new-workflow" should be created expect(result.changeSet.creates).toHaveLength(1); @@ -136,7 +143,14 @@ describe("planWorkflow", () => { "job-a": ["job-a"], }; - const result = await planWorkflow(client, workspaceId, appName, workflows, mainJobDeps); + const result = await planWorkflow( + client, + workspaceId, + appName, + workflows, + mainJobDeps, + new Set(), + ); // "workflow-a" should be updated expect(result.changeSet.updates).toHaveLength(1); @@ -153,7 +167,7 @@ describe("planWorkflow", () => { { id: "2", name: "workflow-2", label: appName }, ]); - const result = await planWorkflow(client, workspaceId, appName, {}, {}); + const result = await planWorkflow(client, workspaceId, appName, {}, {}, new Set()); expect(result.changeSet.deletes).toHaveLength(2); expect(result.changeSet.deletes.map((d) => d.name).sort()).toEqual([ @@ -169,7 +183,7 @@ describe("planWorkflow", () => { { id: "1", name: "unmanaged-workflow" }, // No label ]); - const result = await planWorkflow(client, workspaceId, appName, {}, {}); + const result = await planWorkflow(client, workspaceId, appName, {}, {}, new Set()); expect(result.changeSet.deletes).toHaveLength(0); }); @@ -177,7 +191,7 @@ describe("planWorkflow", () => { test("workflow owned by different app is NOT deleted", async () => { const client = createMockClient([{ id: "1", name: "other-workflow", label: "other-app" }]); - const result = await planWorkflow(client, workspaceId, appName, {}, {}); + const result = await planWorkflow(client, workspaceId, appName, {}, {}, new Set()); expect(result.changeSet.deletes).toHaveLength(0); expect(result.resourceOwners.has("other-app")).toBe(true); @@ -190,7 +204,7 @@ describe("planWorkflow", () => { { id: "3", name: "unmanaged-workflow" }, // No label ]); - const result = await planWorkflow(client, workspaceId, appName, {}, {}); + const result = await planWorkflow(client, workspaceId, appName, {}, {}, new Set()); expect(result.changeSet.deletes).toHaveLength(1); expect(result.changeSet.deletes[0].name).toBe("my-workflow"); diff --git a/packages/sdk/src/cli/commands/apply/workflow.ts b/packages/sdk/src/cli/commands/apply/workflow.ts index 6868e126f..826deb6a2 100644 --- a/packages/sdk/src/cli/commands/apply/workflow.ts +++ b/packages/sdk/src/cli/commands/apply/workflow.ts @@ -1,17 +1,12 @@ import { type ApplyPhase } from "@/cli/commands/apply/apply"; import { parseDuration } from "@/cli/shared/args"; import { type OperatorClient, fetchAll } from "@/cli/shared/client"; -import { logger, styles } from "@/cli/shared/logger"; import { createChangeSet, type ChangeSet, type HasName } from "./change-set"; import { areNormalizedEqual } from "./compare"; import { workflowJobFunctionName } from "./function-registry"; import { - actionSymbol, - buildRemainingFunctionRegistryEntries, - createRelatedFunctionRegistryNameSets, - type DisplayAction, + formatChangeEntriesWithFunctionRegistry, type GroupedDisplayEntry, - type RelatedFunctionRegistryNameSets, type RelatedFunctionRegistryChanges, } from "./grouped-display"; import { buildMetaRequest, hasMatchingSdkVersion, sdkNameLabelKey, type WithLabel } from "./label"; @@ -271,12 +266,6 @@ function jobFunctionTrn(workspaceId: string, name: string) { * @param workflows - Parsed workflows * @param mainJobDeps - Main job dependencies by workflow * @param unchangedJobFunctions - Job functions already proven unchanged by function registry plan - * @param workflowJobFunctionChanges - Related function registry changes for workflow jobs - * @param workflowJobFunctionChanges.creates - Function registry creations - * @param workflowJobFunctionChanges.updates - Function registry updates - * @param workflowJobFunctionChanges.deletes - Function registry deletions - * @param workflowJobFunctionChanges.replaces - Function registry replacements - * @param workflowJobFunctionChanges.unchanged - Function registry unchanged entries * @returns Planned workflow changes */ export async function planWorkflow( @@ -285,14 +274,7 @@ export async function planWorkflow( appName: string, workflows: Record, mainJobDeps: Record, - unchangedJobFunctions: ReadonlySet = new Set(), - workflowJobFunctionChanges?: { - creates: ReadonlyArray; - updates: ReadonlyArray; - deletes: ReadonlyArray; - replaces: ReadonlyArray; - unchanged: ReadonlyArray; - }, + unchangedJobFunctions: ReadonlySet, ) { const changeSet = createChangeSet("Workflows"); const conflicts: OwnerConflict[] = []; @@ -409,7 +391,6 @@ export async function planWorkflow( } }); - printWorkflowChanges(changeSet, workflowJobFunctionChanges); return { changeSet, conflicts, @@ -422,42 +403,10 @@ export async function planWorkflow( type WorkflowDisplayEntry = GroupedDisplayEntry; -function collectWorkflowDisplayEntries< - T extends Pick, ->( - action: DisplayAction, - workflowItems: ReadonlyArray, - workflowJobFunctionNames: ReadonlySet, - consumedWorkflowJobFunctionNames: Set, -) { - return workflowItems.map((item) => { - const matchingFunctionNames = new Set(); - for (const jobName of item.usedJobNames) { - const functionName = workflowJobFunctionName(jobName); - if (workflowJobFunctionNames.has(functionName)) { - matchingFunctionNames.add(functionName); - } - } - for (const functionName of matchingFunctionNames) { - consumedWorkflowJobFunctionNames.add(functionName); - } - return { - action, - symbol: actionSymbol(action), - name: item.name, - labels: matchingFunctionNames.size > 0 ? ["workflow", "functionRegistry"] : ["workflow"], - }; - }); -} - /** * Format workflow changes for grouped dry-run display. * @param changeSet - Workflow changes * @param workflowJobFunctionChanges - Related function registry changes for workflow jobs - * @param workflowJobFunctionChanges.creates - Function registry creations - * @param workflowJobFunctionChanges.updates - Function registry updates - * @param workflowJobFunctionChanges.deletes - Function registry deletions - * @param workflowJobFunctionChanges.replaces - Function registry replacements * @returns Display entries for workflow output */ export function formatWorkflowChangeEntries( @@ -467,57 +416,15 @@ export function formatWorkflowChangeEntries( >, workflowJobFunctionChanges?: RelatedFunctionRegistryChanges, ): WorkflowDisplayEntry[] { - const functionNames = createRelatedFunctionRegistryNameSets(workflowJobFunctionChanges); - const consumed: RelatedFunctionRegistryNameSets = createRelatedFunctionRegistryNameSets(); - - const entries = [ - ...collectWorkflowDisplayEntries( - "create", - changeSet.creates, - functionNames.creates, - consumed.creates, - ), - ...collectWorkflowDisplayEntries( - "delete", - changeSet.deletes, - functionNames.deletes, - consumed.deletes, - ), - ...collectWorkflowDisplayEntries( - "update", - changeSet.updates, - functionNames.updates, - consumed.updates, - ), - ...(changeSet.replaces as ReadonlyArray).map((item) => ({ - action: "replace" as const, - symbol: actionSymbol("replace"), - name: item.name, - labels: ["workflow"], - })), - ...buildRemainingFunctionRegistryEntries(functionNames, consumed), - ]; - return entries; -} - -function printWorkflowChanges( - changeSet: ChangeSet, - workflowJobFunctionChanges?: { - creates: ReadonlyArray; - updates: ReadonlyArray; - deletes: ReadonlyArray; - replaces: ReadonlyArray; - }, -) { - const entries = formatWorkflowChangeEntries(changeSet, workflowJobFunctionChanges); - if (entries.length === 0) { - return; - } - - logger.log(styles.bold("Workflows:")); - for (const entry of entries) { - logger.log(` ${entry.symbol} ${entry.name} (${entry.labels.join(", ")})`); - } + return formatChangeEntriesWithFunctionRegistry( + "workflow", + changeSet, + workflowJobFunctionChanges, + (item) => + "usedJobNames" in item + ? item.usedJobNames.map((jobName) => workflowJobFunctionName(jobName)) + : [], + ); } function canTreatWorkflowAsUnchanged( diff --git a/packages/sdk/src/cli/commands/remove.ts b/packages/sdk/src/cli/commands/remove.ts index e39a48aeb..d0545f256 100644 --- a/packages/sdk/src/cli/commands/remove.ts +++ b/packages/sdk/src/cli/commands/remove.ts @@ -71,7 +71,7 @@ async function execRemove( const pipeline = await planPipeline(ctx); const app = await planApplication(ctx); const executor = await planExecutor(ctx); - const workflow = await planWorkflow(client, workspaceId, application.name, {}, {}); + const workflow = await planWorkflow(client, workspaceId, application.name, {}, {}, new Set()); const functionRegistry = await planFunctionRegistry(client, workspaceId, application.name, []); const secretManager = await planSecretManager(ctx);