Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-inherited-event-version-diffs.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Comment thread
cdarne marked this conversation as resolved.

const events =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<BaseConfigType & typeof localConfig>({
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({
Expand Down
Loading