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

Notification system #4149

Merged
merged 63 commits into from
Nov 8, 2024
Merged

Notification system #4149

merged 63 commits into from
Nov 8, 2024

Conversation

gonzaloriestra
Copy link
Contributor

@gonzaloriestra gonzaloriestra commented Jul 3, 2024

WHY are these changes introduced?

Fixes: https://github.com/Shopify/develop-app-inner-loop/issues/1962

We want to notify CLI users about deprecations, new features or events

WHAT is this pull request doing?

Adds three hidden commands:

  • notifications generate: shows several prompts asking for the notification content, type and filters, and adds it to a local notifications.json file.
  • notifications list: shows the current notifications from the local notifications.json
  • cache clear: clears the CLI cache (Mac stores it in ~/Library/Preferences/shopify-cli-kit-nodejs/config.json), including notifications stuff and other API responses that are temporarily memorized

Then, before each command we check the remote notifications.json from this repository and show the required ones. The file is cached for 24h.

Demo: https://hackdays.shopify.io/projects/18981

How to test your changes?

Install the snapshot from this branch: npm i -g @shopify/[email protected]

I've uploaded a sample notifications.json to simplify testing:

  • SHOPIFY_CLI_NOTIFICATIONS_URL=https://shopify.link/MNbn shopify version
  • SHOPIFY_CLI_NOTIFICATIONS_URL=https://shopify.link/MNbn shopify theme list

When SHOPIFY_CLI_NOTIFICATIONS_URL is not passed, it looks for the file in the main branch of this repository (it doesn't exist yet).

To generate and list notifications:

  • shopify notifications generate
  • shopify notifications list

To clear the cache, so that it downloads again the configuration file and forgets if a notification was shown:

  • shopify cache clear

Post-release steps

Merge the internal doc: https://github.com/Shopify/vault-pages/pull/7111

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

@gonzaloriestra gonzaloriestra added the includes-post-release-steps PRs including this label require additional steps after releasing label Jul 3, 2024
Copy link
Contributor

github-actions bot commented Jul 3, 2024

Thanks for your contribution!

Depending on what you are working on, you may want to request a review from a Shopify team:

  • Themes: @shopify/advanced-edits
  • UI extensions: @shopify/ui-extensions-cli
    • Checkout UI extensions: @shopify/checkout-ui-extensions-api-stewardship
  • Hydrogen: @shopify/hydrogen
  • Other: @shopify/app-management

@gonzaloriestra
Copy link
Contributor Author

/snapit

Copy link
Contributor

🫰✨ Thanks @gonzaloriestra! Your snapshot has been published to npm.

Test the snapshot by intalling your package globally:

pnpm i -g @shopify/[email protected]

After installing, validate the version by running just shopify in your terminal
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

Copy link
Contributor

This PR seems inactive. If it's still relevant, please add a comment saying so. Otherwise, take no action.
→ If there's no activity within a week, then a bot will automatically close this.
Thanks for helping to improve Shopify's dev tooling and experience.

This comment has been minimized.

@gonzaloriestra
Copy link
Contributor Author

/snapit

Copy link
Contributor

github-actions bot commented Nov 8, 2024

🫰✨ Thanks @gonzaloriestra! Your snapshot has been published to npm.

Test the snapshot by intalling your package globally:

pnpm i -g @shopify/[email protected]

After installing, validate the version by running just shopify in your terminal
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

Copy link
Contributor

github-actions bot commented Nov 8, 2024

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/public/node/global-context.d.ts
export interface GlobalContext {
    currentCommandId: string;
}
/**
 * Get the current command ID.
 *
 * @returns Current command ID.
 */
export declare function getCurrentCommandId(): string;
/**
 * Set the current command ID.
 *
 * @param commandId - Command ID.
 */
export declare function setCurrentCommandId(commandId: string): void;
packages/cli-kit/dist/public/node/notifications-system.d.ts
import { zod } from './schema.js';
declare const NotificationSchema: zod.ZodObject<{
    id: zod.ZodString;
    message: zod.ZodString;
    type: zod.ZodEnum<["info", "warning", "error"]>;
    frequency: zod.ZodEnum<["always", "once", "once_a_day", "once_a_week"]>;
    ownerChannel: zod.ZodString;
    cta: zod.ZodOptional<zod.ZodObject<{
        label: zod.ZodString;
        url: zod.ZodString;
    }, "strip", zod.ZodTypeAny, {
        url: string;
        label: string;
    }, {
        url: string;
        label: string;
    }>>;
    title: zod.ZodOptional<zod.ZodString>;
    minVersion: zod.ZodOptional<zod.ZodString>;
    maxVersion: zod.ZodOptional<zod.ZodString>;
    minDate: zod.ZodOptional<zod.ZodString>;
    maxDate: zod.ZodOptional<zod.ZodString>;
    commands: zod.ZodOptional<zod.ZodArray<zod.ZodString, "many">>;
    surface: zod.ZodOptional<zod.ZodString>;
}, "strip", zod.ZodTypeAny, {
    id: string;
    type: "error" | "info" | "warning";
    message: string;
    frequency: "always" | "once" | "once_a_day" | "once_a_week";
    ownerChannel: string;
    cta?: {
        url: string;
        label: string;
    } | undefined;
    title?: string | undefined;
    minVersion?: string | undefined;
    maxVersion?: string | undefined;
    minDate?: string | undefined;
    maxDate?: string | undefined;
    commands?: string[] | undefined;
    surface?: string | undefined;
}, {
    id: string;
    type: "error" | "info" | "warning";
    message: string;
    frequency: "always" | "once" | "once_a_day" | "once_a_week";
    ownerChannel: string;
    cta?: {
        url: string;
        label: string;
    } | undefined;
    title?: string | undefined;
    minVersion?: string | undefined;
    maxVersion?: string | undefined;
    minDate?: string | undefined;
    maxDate?: string | undefined;
    commands?: string[] | undefined;
    surface?: string | undefined;
}>;
export type Notification = zod.infer<typeof NotificationSchema>;
declare const NotificationsSchema: zod.ZodObject<{
    notifications: zod.ZodArray<zod.ZodObject<{
        id: zod.ZodString;
        message: zod.ZodString;
        type: zod.ZodEnum<["info", "warning", "error"]>;
        frequency: zod.ZodEnum<["always", "once", "once_a_day", "once_a_week"]>;
        ownerChannel: zod.ZodString;
        cta: zod.ZodOptional<zod.ZodObject<{
            label: zod.ZodString;
            url: zod.ZodString;
        }, "strip", zod.ZodTypeAny, {
            url: string;
            label: string;
        }, {
            url: string;
            label: string;
        }>>;
        title: zod.ZodOptional<zod.ZodString>;
        minVersion: zod.ZodOptional<zod.ZodString>;
        maxVersion: zod.ZodOptional<zod.ZodString>;
        minDate: zod.ZodOptional<zod.ZodString>;
        maxDate: zod.ZodOptional<zod.ZodString>;
        commands: zod.ZodOptional<zod.ZodArray<zod.ZodString, "many">>;
        surface: zod.ZodOptional<zod.ZodString>;
    }, "strip", zod.ZodTypeAny, {
        id: string;
        type: "error" | "info" | "warning";
        message: string;
        frequency: "always" | "once" | "once_a_day" | "once_a_week";
        ownerChannel: string;
        cta?: {
            url: string;
            label: string;
        } | undefined;
        title?: string | undefined;
        minVersion?: string | undefined;
        maxVersion?: string | undefined;
        minDate?: string | undefined;
        maxDate?: string | undefined;
        commands?: string[] | undefined;
        surface?: string | undefined;
    }, {
        id: string;
        type: "error" | "info" | "warning";
        message: string;
        frequency: "always" | "once" | "once_a_day" | "once_a_week";
        ownerChannel: string;
        cta?: {
            url: string;
            label: string;
        } | undefined;
        title?: string | undefined;
        minVersion?: string | undefined;
        maxVersion?: string | undefined;
        minDate?: string | undefined;
        maxDate?: string | undefined;
        commands?: string[] | undefined;
        surface?: string | undefined;
    }>, "many">;
}, "strip", zod.ZodTypeAny, {
    notifications: {
        id: string;
        type: "error" | "info" | "warning";
        message: string;
        frequency: "always" | "once" | "once_a_day" | "once_a_week";
        ownerChannel: string;
        cta?: {
            url: string;
            label: string;
        } | undefined;
        title?: string | undefined;
        minVersion?: string | undefined;
        maxVersion?: string | undefined;
        minDate?: string | undefined;
        maxDate?: string | undefined;
        commands?: string[] | undefined;
        surface?: string | undefined;
    }[];
}, {
    notifications: {
        id: string;
        type: "error" | "info" | "warning";
        message: string;
        frequency: "always" | "once" | "once_a_day" | "once_a_week";
        ownerChannel: string;
        cta?: {
            url: string;
            label: string;
        } | undefined;
        title?: string | undefined;
        minVersion?: string | undefined;
        maxVersion?: string | undefined;
        minDate?: string | undefined;
        maxDate?: string | undefined;
        commands?: string[] | undefined;
        surface?: string | undefined;
    }[];
}>;
export type Notifications = zod.infer<typeof NotificationsSchema>;
/**
 * Shows notifications to the user if they meet the criteria specified in the notifications.json file.
 *
 * @param currentSurfaces - The surfaces present in the current project (usually for app extensions).
 * @returns - A promise that resolves when the notifications have been shown.
 */
export declare function showNotificationsIfNeeded(currentSurfaces?: string[]): Promise<void>;
/**
 * Get notifications list from cache (refreshed every hour) or fetch it if not present.
 *
 * @returns A Notifications object.
 */
export declare function getNotifications(): Promise<Notifications>;
/**
 * Filters notifications based on the version of the CLI.
 *
 * @param notifications - The notifications to filter.
 * @param commandId - The command ID to filter by.
 * @param currentSurfaces - The surfaces present in the current project (usually for app extensions).
 * @param today - The current date.
 * @param currentVersion - The current version of the CLI.
 * @returns - The filtered notifications.
 */
export declare function filterNotifications(notifications: Notification[], commandId: string, currentSurfaces?: string[], today?: Date, currentVersion?: string): Notification[];
/**
 * Returns a string with the filters from a notification, one by line.
 *
 * @param notification - The notification to get the filters from.
 * @returns A string with human-readable filters from the notification.
 */
export declare function stringifyFilters(notification: Notification): string;
/**
 * Reads the notifications from the local file.
 *
 * @returns A Notifications object.
 */
export declare function getLocalNotifications(): Promise<Notifications>;
export {};

Existing type declarations

packages/cli-kit/dist/private/node/conf-store.d.ts
@@ -5,13 +5,17 @@ interface CacheValue<T> {
 }
 export type IntrospectionUrlKey = ;
 export type PackageVersionKey = ;
+export type NotificationsKey = ;
+export type NotificationKey = ;
 type MostRecentOccurrenceKey = ;
 type RateLimitKey = ;
-type ExportedKey = IntrospectionUrlKey | PackageVersionKey;
+type ExportedKey = IntrospectionUrlKey | PackageVersionKey | NotificationsKey | NotificationKey;
 interface Cache {
     [introspectionUrlKey: IntrospectionUrlKey]: CacheValue<string>;
     [packageVersionKey: PackageVersionKey]: CacheValue<string>;
-    [mostRecentOccurrenceKey: MostRecentOccurrenceKey]: CacheValue<boolean>;
+    [notifications: NotificationsKey]: CacheValue<string>;
+    [notification: NotificationKey]: CacheValue<string>;
+    [MostRecentOccurrenceKey: MostRecentOccurrenceKey]: CacheValue<boolean>;
     [rateLimitKey: RateLimitKey]: CacheValue<number[]>;
 }
 export interface ConfSchema {
@@ -45,12 +49,13 @@ type CacheValueForKey<TKey extends keyof Cache> = NonNullable<Cache[TKey]>['valu
  * @returns The value from the cache or the result of the function.
  */
 export declare function cacheRetrieveOrRepopulate(key: ExportedKey, fn: () => Promise<CacheValueForKey<typeof key>>, timeout?: number, config?: LocalStorage<ConfSchema>): Promise<CacheValueForKey<typeof key>>;
+export declare function cacheStore(key: ExportedKey, value: string, config?: LocalStorage<ConfSchema>): void;
 /**
  * Fetch from cache if already populated, otherwise return undefined.
  * @param key - The key to use for the cache.
- * @returns The value from the cache or the result of the function.
+ * @returns The chache element.
  */
-export declare function cacheRetrieve(key: ExportedKey, config?: LocalStorage<ConfSchema>): CacheValueForKey<typeof key> | undefined;
+export declare function cacheRetrieve(key: ExportedKey, config?: LocalStorage<ConfSchema>): CacheValue<string> | undefined;
 export declare function cacheClear(config?: LocalStorage<ConfSchema>): void;
 interface TimeInterval {
     days?: number;
packages/cli-kit/dist/public/node/cli.d.ts
@@ -34,4 +34,8 @@ export declare const globalFlags: {
     'no-color': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
     verbose: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
 };
+/**
+ * Clear the CLI cache, used to store some API responses and handle notifications status
+ */
+export declare function clearCache(): void;
 export {};
\ No newline at end of file

@gonzaloriestra gonzaloriestra added this pull request to the merge queue Nov 8, 2024
Merged via the queue into main with commit aabbb9c Nov 8, 2024
27 checks passed
@gonzaloriestra gonzaloriestra deleted the notifications branch November 8, 2024 17:05
@shauns shauns restored the notifications branch November 8, 2024 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
includes-post-release-steps PRs including this label require additional steps after releasing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants