From e2e1eec14eae9bb67a5d6fadff2f7d13a3134294 Mon Sep 17 00:00:00 2001 From: Cedric Darne Date: Mon, 28 Sep 2026 17:31:51 -0400 Subject: [PATCH] Fix repeated event config diffs when API versions are inherited MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core fills [events].api_version into event subscriptions that omit it, so the remote config carries a version the local TOML doesn't. Drop a subscription api_version equal to the events default when reading remote config. Deploy compares both sides through this transform, so unchanged events no longer show as updated, and config link only writes overrides. Co-Authored-By: Pierre-Olivier Bédard --- .../fix-inherited-event-version-diffs.md | 5 + .../transform/app_config_events.test.ts | 24 ++++ .../transform/app_config_events.ts | 14 ++- .../deploy-identifier-matching.test.ts | 108 ++++++++++++++++++ 4 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-inherited-event-version-diffs.md diff --git a/.changeset/fix-inherited-event-version-diffs.md b/.changeset/fix-inherited-event-version-diffs.md new file mode 100644 index 00000000000..fa350faa167 --- /dev/null +++ b/.changeset/fix-inherited-event-version-diffs.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix unchanged event subscriptions appearing as updates during app deploy and avoid redundant subscription API versions when linking config. diff --git a/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts b/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts index f78346c1a2f..a744bcd8be5 100644 --- a/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts +++ b/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts @@ -176,6 +176,30 @@ describe('transformToEventsConfig', () => { }) }) + test('strips subscription api_version matching the events default while keeping overrides', () => { + const remoteContent = { + events: { + api_version: '2024-01', + subscription: [ + {topic: 'orders/create', uri: 'https://example.com/a', api_version: '2024-01', identifier: 'id-a'}, + {topic: 'products/update', uri: 'https://example.com/b', api_version: '2025-07', identifier: 'id-b'}, + ], + }, + } + + const result = transformToEventsConfig(remoteContent) + + expect(result).toEqual({ + events: { + api_version: '2024-01', + subscription: [ + {topic: 'orders/create', uri: 'https://example.com/a'}, + {topic: 'products/update', uri: 'https://example.com/b', api_version: '2025-07'}, + ], + }, + }) + }) + test('handles missing subscription field', () => { const remoteContent = { events: { diff --git a/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts b/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts index da5883ce1de..b811be56baa 100644 --- a/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts +++ b/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts @@ -41,17 +41,19 @@ export function transformFromEventsConfig(content: object, appConfiguration?: ob /** * Transforms the events config from remote to local format. - * Strips the server-managed 'identifier' field from subscriptions. + * Strips the server-managed 'identifier' field from subscriptions, and the + * subscription 'api_version' when it matches the events default. */ export function transformToEventsConfig(content: object) { const eventsConfig = getPathValue(content, 'events') as {api_version: string; subscription: object[]} - const apiVersion = getPathValue(eventsConfig, 'api_version') - const subscription = getPathValue(eventsConfig, 'subscription') as {identifier: string}[] + const apiVersion = getPathValue(eventsConfig, 'api_version') as string + const subscription = getPathValue(eventsConfig, 'subscription') as {identifier: string; api_version?: string}[] - // Server always includes identifier - strip it for local TOML + // Server adds identifier and fills [events].api_version into subscriptions that omit it const cleanedSubscriptions = subscription?.map((sub) => { - const {identifier, ...rest} = sub - return rest + const {identifier, api_version: subscriptionApiVersion, ...rest} = sub + const overridesDefault = subscriptionApiVersion !== undefined && subscriptionApiVersion !== apiVersion + return overridesDefault ? {...rest, api_version: subscriptionApiVersion} : rest }) const events = diff --git a/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts b/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts index 1b24ebfc74b..64b951d285e 100644 --- a/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts +++ b/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts @@ -15,6 +15,7 @@ import {OrganizationApp} from '../../models/organization.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {BaseConfigType} from '../../models/extensions/schemas.js' import {createConfigExtensionSpecification} from '../../models/extensions/specification.js' +import appEventsSpec from '../../models/extensions/specifications/app_config_events.js' import {AppModuleVersion, DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {deployOrReleaseConfirmationPrompt} from '../../prompts/deploy-release.js' import {migrateExtensionsToUIExtension} from '../dev/migrate-to-ui-extension.js' @@ -623,6 +624,113 @@ describe('classifyDeployExtensionChanges', () => { }) describe('ensureDeployIdentifiersFromAppVersion', () => { + test.each([ + { + name: 'inherited version', + localDefault: '2026-07', + localVersion: undefined, + remoteVersion: '2026-07', + updated: false, + }, + { + name: 'explicit default', + localDefault: '2026-07', + localVersion: '2026-07', + remoteVersion: '2026-07', + updated: false, + }, + { + name: 'explicit override', + localDefault: '2026-07', + localVersion: '2026-04', + remoteVersion: '2026-04', + updated: false, + }, + { + name: 'changed override', + localDefault: '2026-07', + localVersion: '2026-04', + remoteVersion: '2026-07', + updated: true, + }, + { + name: 'removed override', + localDefault: '2026-07', + localVersion: undefined, + remoteVersion: '2026-04', + updated: true, + }, + { + name: 'changed default', + localDefault: '2026-10', + localVersion: undefined, + remoteVersion: '2026-07', + updated: true, + }, + ])('reports events updated=$updated for $name', async ({localDefault, localVersion, remoteVersion, updated}) => { + const subscription = { + handle: 'product-updated', + topic: 'Product', + actions: ['update'], + uri: '/events', + } + const localConfig = { + events: { + api_version: localDefault, + subscription: [{...subscription, ...(localVersion === undefined ? {} : {api_version: localVersion})}], + }, + } + const eventsExtension = new ExtensionInstance({ + configuration: localConfig, + configurationPath: 'shopify.app.toml', + directory: '/app', + specification: appEventsSpec, + }) + const remoteEventsModule: AppModuleVersion = { + registrationId: eventsExtension.uid, + registrationUuid: 'events-uuid', + registrationTitle: 'Events', + type: 'events', + config: { + events: { + api_version: '2026-07', + subscription: [ + { + ...subscription, + uri: 'https://example.com/events', + api_version: remoteVersion, + identifier: 'subscription-id', + }, + ], + }, + }, + specification: { + identifier: 'events', + name: 'Events', + experience: 'configuration', + options: {managementExperience: 'cli'}, + }, + } + + await ensureDeployIdentifiersFromAppVersion( + deployOptions({ + app: testApp({...APP, allExtensions: [eventsExtension], specifications: [appEventsSpec]}), + activeAppVersion: {appModuleVersions: [remoteEventsModule]}, + }), + ) + + expect(deployOrReleaseConfirmationPrompt).toHaveBeenCalledWith( + expect.objectContaining({ + configExtensionIdentifiersBreakdown: { + existingFieldNames: updated ? [] : ['events'], + existingUpdatedFieldNames: updated ? ['events'] : [], + newFieldNames: [], + deletedFieldNames: [], + }, + }), + ) + }) + test('prompts with the existing UI breakdown shape and returns deploy identifiers', async () => { const identifiers = await ensureDeployIdentifiersFromAppVersion( deployOptions({