Skip to content

Commit e2e1eec

Browse files
cdarnepobed2
andcommitted
Fix repeated event config diffs when API versions are inherited
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 <pobed2@gmail.com>
1 parent 85d0050 commit e2e1eec

4 files changed

Lines changed: 145 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/app': patch
3+
---
4+
5+
Fix unchanged event subscriptions appearing as updates during app deploy and avoid redundant subscription API versions when linking config.

‎packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,30 @@ describe('transformToEventsConfig', () => {
176176
})
177177
})
178178

179+
test('strips subscription api_version matching the events default while keeping overrides', () => {
180+
const remoteContent = {
181+
events: {
182+
api_version: '2024-01',
183+
subscription: [
184+
{topic: 'orders/create', uri: 'https://example.com/a', api_version: '2024-01', identifier: 'id-a'},
185+
{topic: 'products/update', uri: 'https://example.com/b', api_version: '2025-07', identifier: 'id-b'},
186+
],
187+
},
188+
}
189+
190+
const result = transformToEventsConfig(remoteContent)
191+
192+
expect(result).toEqual({
193+
events: {
194+
api_version: '2024-01',
195+
subscription: [
196+
{topic: 'orders/create', uri: 'https://example.com/a'},
197+
{topic: 'products/update', uri: 'https://example.com/b', api_version: '2025-07'},
198+
],
199+
},
200+
})
201+
})
202+
179203
test('handles missing subscription field', () => {
180204
const remoteContent = {
181205
events: {

‎packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,19 @@ export function transformFromEventsConfig(content: object, appConfiguration?: ob
4141

4242
/**
4343
* Transforms the events config from remote to local format.
44-
* Strips the server-managed 'identifier' field from subscriptions.
44+
* Strips the server-managed 'identifier' field from subscriptions, and the
45+
* subscription 'api_version' when it matches the events default.
4546
*/
4647
export function transformToEventsConfig(content: object) {
4748
const eventsConfig = getPathValue(content, 'events') as {api_version: string; subscription: object[]}
48-
const apiVersion = getPathValue(eventsConfig, 'api_version')
49-
const subscription = getPathValue(eventsConfig, 'subscription') as {identifier: string}[]
49+
const apiVersion = getPathValue(eventsConfig, 'api_version') as string
50+
const subscription = getPathValue(eventsConfig, 'subscription') as {identifier: string; api_version?: string}[]
5051

51-
// Server always includes identifier - strip it for local TOML
52+
// Server adds identifier and fills [events].api_version into subscriptions that omit it
5253
const cleanedSubscriptions = subscription?.map((sub) => {
53-
const {identifier, ...rest} = sub
54-
return rest
54+
const {identifier, api_version: subscriptionApiVersion, ...rest} = sub
55+
const overridesDefault = subscriptionApiVersion !== undefined && subscriptionApiVersion !== apiVersion
56+
return overridesDefault ? {...rest, api_version: subscriptionApiVersion} : rest
5557
})
5658

5759
const events =

‎packages/app/src/cli/services/context/deploy-identifier-matching.test.ts‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {OrganizationApp} from '../../models/organization.js'
1515
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
1616
import {BaseConfigType} from '../../models/extensions/schemas.js'
1717
import {createConfigExtensionSpecification} from '../../models/extensions/specification.js'
18+
import appEventsSpec from '../../models/extensions/specifications/app_config_events.js'
1819
import {AppModuleVersion, DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
1920
import {deployOrReleaseConfirmationPrompt} from '../../prompts/deploy-release.js'
2021
import {migrateExtensionsToUIExtension} from '../dev/migrate-to-ui-extension.js'
@@ -623,6 +624,113 @@ describe('classifyDeployExtensionChanges', () => {
623624
})
624625

625626
describe('ensureDeployIdentifiersFromAppVersion', () => {
627+
test.each([
628+
{
629+
name: 'inherited version',
630+
localDefault: '2026-07',
631+
localVersion: undefined,
632+
remoteVersion: '2026-07',
633+
updated: false,
634+
},
635+
{
636+
name: 'explicit default',
637+
localDefault: '2026-07',
638+
localVersion: '2026-07',
639+
remoteVersion: '2026-07',
640+
updated: false,
641+
},
642+
{
643+
name: 'explicit override',
644+
localDefault: '2026-07',
645+
localVersion: '2026-04',
646+
remoteVersion: '2026-04',
647+
updated: false,
648+
},
649+
{
650+
name: 'changed override',
651+
localDefault: '2026-07',
652+
localVersion: '2026-04',
653+
remoteVersion: '2026-07',
654+
updated: true,
655+
},
656+
{
657+
name: 'removed override',
658+
localDefault: '2026-07',
659+
localVersion: undefined,
660+
remoteVersion: '2026-04',
661+
updated: true,
662+
},
663+
{
664+
name: 'changed default',
665+
localDefault: '2026-10',
666+
localVersion: undefined,
667+
remoteVersion: '2026-07',
668+
updated: true,
669+
},
670+
])('reports events updated=$updated for $name', async ({localDefault, localVersion, remoteVersion, updated}) => {
671+
const subscription = {
672+
handle: 'product-updated',
673+
topic: 'Product',
674+
actions: ['update'],
675+
uri: '/events',
676+
}
677+
const localConfig = {
678+
events: {
679+
api_version: localDefault,
680+
subscription: [{...subscription, ...(localVersion === undefined ? {} : {api_version: localVersion})}],
681+
},
682+
}
683+
const eventsExtension = new ExtensionInstance<BaseConfigType & typeof localConfig>({
684+
configuration: localConfig,
685+
configurationPath: 'shopify.app.toml',
686+
directory: '/app',
687+
specification: appEventsSpec,
688+
})
689+
const remoteEventsModule: AppModuleVersion = {
690+
registrationId: eventsExtension.uid,
691+
registrationUuid: 'events-uuid',
692+
registrationTitle: 'Events',
693+
type: 'events',
694+
config: {
695+
events: {
696+
api_version: '2026-07',
697+
subscription: [
698+
{
699+
...subscription,
700+
uri: 'https://example.com/events',
701+
api_version: remoteVersion,
702+
identifier: 'subscription-id',
703+
},
704+
],
705+
},
706+
},
707+
specification: {
708+
identifier: 'events',
709+
name: 'Events',
710+
experience: 'configuration',
711+
options: {managementExperience: 'cli'},
712+
},
713+
}
714+
715+
await ensureDeployIdentifiersFromAppVersion(
716+
deployOptions({
717+
app: testApp({...APP, allExtensions: [eventsExtension], specifications: [appEventsSpec]}),
718+
activeAppVersion: {appModuleVersions: [remoteEventsModule]},
719+
}),
720+
)
721+
722+
expect(deployOrReleaseConfirmationPrompt).toHaveBeenCalledWith(
723+
expect.objectContaining({
724+
configExtensionIdentifiersBreakdown: {
725+
existingFieldNames: updated ? [] : ['events'],
726+
existingUpdatedFieldNames: updated ? ['events'] : [],
727+
newFieldNames: [],
728+
deletedFieldNames: [],
729+
},
730+
}),
731+
)
732+
})
733+
626734
test('prompts with the existing UI breakdown shape and returns deploy identifiers', async () => {
627735
const identifiers = await ensureDeployIdentifiersFromAppVersion(
628736
deployOptions({

0 commit comments

Comments
 (0)