Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploy multiple webhook subscription modules #3764

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

gracejychang
Copy link
Contributor

@gracejychang gracejychang commented Apr 20, 2024

WHY are these changes introduced?

Fixes https://github.com/Shopify/develop-app-management/issues/1720 and https://github.com/Shopify/develop-app-management/issues/1724

See https://github.com/Shopify/develop-app-management/issues/1720 for more details and testing instructions

WHAT is this pull request doing?

How to test your changes?

Post-release steps

Measuring impact

How do we know this change was effective? Please choose one:

  • n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
  • Existing analytics will cater for this addition
  • PR includes analytics changes to measure impact

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've made sure that any changes to dev or deploy have been reflected in the internal flowchart.

Copy link
Contributor

We detected some changes at either packages/*/src or packages/cli-kit/assets/cli-ruby/** and there are no updates in the .changeset.
If the changes are user-facing, run "pnpm changeset add" to track your changes and include them in the next release CHANGELOG.

@karenxie karenxie marked this pull request as draft April 22, 2024 19:42
Copy link
Contributor

github-actions bot commented Apr 23, 2024

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements
71.98% (+0.2% 🔼)
7010/9739
🟡 Branches
68.85% (+0.16% 🔼)
3457/5021
🟡 Functions
71.5% (+0.28% 🔼)
1884/2635
🟡 Lines
73.28% (+0.2% 🔼)
6608/9018
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🟢
... / app.test-data.ts
93.06% (+0.25% 🔼)
93.83% (-0.98% 🔻)
83.56% (+0.46% 🔼)
92.5% (+0.24% 🔼)
🟢
... / breakdown-extensions.ts
96.49% (+0.06% 🔼)
82.76% (-0.57% 🔻)
100% 100%

Test suite run success

1641 tests passing in 766 suites.

Report generated by 🧪jest coverage report action from 017baff

@karenxie karenxie force-pushed the deploy-multiple-modules branch 5 times, most recently from 53ef185 to e9bc505 Compare April 25, 2024 21:33
@karenxie karenxie changed the title Deploy multiple modules Deploy multiple webhook subscription modules Apr 26, 2024
if (topics)
return topics.map((topic) => {
return {api_version, uri, topic, ...optionalFields}
})
})

return webhookSubscriptions.length > 0 ? webhookSubscriptions : []
return webhookSubscriptions.length > 0 ? webhookSubscriptions.filter(Boolean) : []
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fancy way of filtering out undefined values we get from subscription objects that might not have topics (ie. in the case of privacy compliance webhooks)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some utils for this kind of operations. To filter undefineds from an array, use getArrayRejectingUndefined :)

@@ -50,14 +50,15 @@ function transformFromWebhookSubscriptionConfig(content: object) {
const {api_version, subscriptions = []} = webhooks

const webhookSubscriptions = subscriptions.flatMap((subscription) => {
const {uri, topics, ...optionalFields} = subscription
// compliance_topics gets handled by privacy_compliance_webhooks
const {uri, topics, compliance_topics: _, ...optionalFields} = subscription
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed compliance_topics: _ to prevent compliance_topics to get pushed as part of subscription in the webhook_subscription config, which gives us errors

@@ -150,7 +150,7 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
}

isUuidManaged() {
return !this.isAppConfigExtension && !this.specification.extensionManagedInToml
return !this.isAppConfigExtension && this.specification.uidStrategy === 'uuid'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this also be !this.specification.uidStrategy === 'dynamic'?

@@ -274,7 +274,9 @@ async function ensureExtensionIdsForExtensionsManagedInToml(
developerPlatformClient: DeveloperPlatformClient,
appId: string,
) {
const extensionsManagedInToml = localExtensionRegistrations.filter((ext) => ext.specification.extensionManagedInToml)
const extensionsManagedInToml = localExtensionRegistrations.filter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably rename these vars as well

Copy link
Contributor

github-actions bot commented May 1, 2024

Unused types (1)

Filename types
packages/app/src/cli/models/extensions/specification.ts UidStrategy

@@ -301,12 +303,19 @@ async function ensureExtensionIdsForExtensionsManagedInToml(
localConfigArray?.forEach((localConfigObj: any) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hasMatch = possibleMatches?.some((possibleMatch: any) => {
const remoteConfigString = possibleMatch.activeVersion?.config
const remoteConfigObj = remoteConfigString ? JSON.parse(remoteConfigString) : ''
const remoteActiveVersionConfig = possibleMatch.activeVersion?.config
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we were only checking against the activeVersion (which is null in cases when we add a new subscription then run dev), we were creating new webhook-subscription drafts, so added new checks for to check against the draftVersion too

const configContent = await extension.commonDeployConfig('')
return Array.isArray(configContent) ? configContent : [configContent]
}

function mapExtensionsIdsNonUuidManaged(extensionsIdsNonUuidManaged: {[key: string]: string[]}) {
const result: {[key: string]: string} = {}
for (const key in extensionsIdsNonUuidManaged) {
if (extensionsIdsNonUuidManaged[key]!.length > 0) {
if (extensionsIdsNonUuidManaged[key]!.length > 1) {
result[key] = extensionsIdsNonUuidManaged[key]!.toString()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind of a hack :/ basically if there's more than 1 id we turn the entire array into a string (then we turn it back into an array later when we're updating the drafts 😬 )

there might be some other downstream effects that this causes

stdout,
stderr,
}: UpdateExtensionDraftOptions) {
const registrationIdArr = registrationId.split(',')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is such a hack lol.. basically if there are multiple registration ids, the data shape is something like:

{ webhook-subscription: '1250,1251'}

(bc I just turned the entire array into a string in mapExtensionsIdsNonUuidManaged)
So the split here turns this back into an array

[1250, 1251]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will modify the type here to be [key: string]: string[]. Non multiples extensions will only have one element inside the array

Anyway, I have a question here.

  • When we deploy the subscriptions, if the local content doesn't match the content of any remote module, then we create a new registrationId (I mean there's no update, the subscriptions are only created or deleted)
  • This updateDraft, is triggered by the dev command when the user modifies the content of the toml, and in this case we are reusing the registrationIds


// eslint-disable-next-line @typescript-eslint/no-misused-promises
registrationIdArr.forEach(async (registrationId, index) => {
const config = configArr[index] as object
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on this? 😬 feels a bit hacky /prone to bugs - just the quickest way I could get individual subscriptions stored in config in the draft versions of webhook-subscription

Comment on lines +268 to +274
if (
specificationIdentifier === WebhookSubscriptionSpecIdentifier &&
remoteConfigObj.topic === localConfig.topic &&
remoteConfigObj.uri === localConfig.uri
)
return true
return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (
specificationIdentifier === WebhookSubscriptionSpecIdentifier &&
remoteConfigObj.topic === localConfig.topic &&
remoteConfigObj.uri === localConfig.uri
)
return true
return false
return specificationIdentifier === WebhookSubscriptionSpecIdentifier &&
remoteConfigObj.topic === localConfig.topic &&
remoteConfigObj.uri === localConfig.uri

@@ -318,6 +318,7 @@ export class App implements AppInterface {
return configuration as CurrentAppConfiguration & SpecsAppConfiguration
}

// karen.xie: do we need to also filter the webhook_subscription modules here?
private filterDeclarativeWebhooksConfig() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't want to manage extension from these specs:

  • privacy_compliance_webhooks
  • webhook_subscription

You should gate those specs behind the flag Flag.DeclarativeWebhooks in partners like this one

That way the final toml schema won't include them so the extensions are not loaded (in fact, you'll receive an parsing error in case the content is there an the beta is not enabled)

I thing that you could also remove fetching the Flag.DeclarativeWebhooks because you don't use it in elsewhere

const remoteActiveVersionConfigObj = remoteActiveVersionConfig ? JSON.parse(remoteActiveVersionConfig) : ''
const remoteDraftVersionConfigObj = remoteDraftVersionConfig ? JSON.parse(remoteDraftVersionConfig) : ''
if (
matchesRemoteConfigForWebhookSubscriptions(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need an explicit matchesRemoteConfigForWebhookSubscriptions?, I mean we could just check the .config regardless the type of extension if they have the uidStrategy === 'dynamic' right?.

@@ -40,8 +41,19 @@ export const pushUpdatesForDraftableExtensions: DevProcessFunction<DraftableExte
await extension.build({app, stdout, stderr, useTasks: false, signal, environment: 'development'})
const registrationId = remoteExtensions[extension.localIdentifier]
if (!registrationId) throw new AbortError(`Extension ${extension.localIdentifier} not found on remote app.`)
// Initial draft update for each extension
await updateExtensionDraft({extension, developerPlatformClient, apiKey, registrationId, stdout, stderr})
if (extension.specification.externalIdentifier === WebhookSubscriptionSpecIdentifier) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't do this inside.

I will create a function that returns the list of final extension instances before the Promise.all

The extension instances that include multipleConfig, will return a copy of the ExtensionInstance for each item, and the only difference could be the config right?

This way you don't need to deal with multiple methods

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway this
if (extension.specification.externalIdentifier === WebhookSubscriptionSpecIdentifier) should be if (await multipleConfigs(extension)) right?

stdout,
stderr,
}: UpdateExtensionDraftOptions) {
const registrationIdArr = registrationId.split(',')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will modify the type here to be [key: string]: string[]. Non multiples extensions will only have one element inside the array

Anyway, I have a question here.

  • When we deploy the subscriptions, if the local content doesn't match the content of any remote module, then we create a new registrationId (I mean there's no update, the subscriptions are only created or deleted)
  • This updateDraft, is triggered by the dev command when the user modifies the content of the toml, and in this case we are reusing the registrationIds

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants