cli icon indicating copy to clipboard operation
cli copied to clipboard

Notification system

Open gonzaloriestra opened this issue 1 year ago โ€ข 9 comments

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 Merge the caution tape bot PR: https://github.com/Shopify/services-db/pull/25841

Measuring impact

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

  • [x] 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

  • [x] I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • [x] I've considered possible documentation changes

gonzaloriestra avatar Jul 03 '24 07:07 gonzaloriestra

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

github-actions[bot] avatar Jul 03 '24 07:07 github-actions[bot]

Coverage report

St.:grey_question:
Category Percentage Covered / Total
๐ŸŸก Statements
71.92% (-0.13% ๐Ÿ”ป)
8440/11735
๐ŸŸก Branches
68.34% (-0.15% ๐Ÿ”ป)
4088/5982
๐ŸŸก Functions
71.47% (+0.13% ๐Ÿ”ผ)
2219/3105
๐ŸŸก Lines
72.33% (-0.1% ๐Ÿ”ป)
7978/11030
Show new covered files ๐Ÿฃ
St.:grey_question:
File Statements Branches Functions Lines
๐Ÿ”ด
... / clear.ts
0% 100% 0% 0%
๐Ÿ”ด
... / generate.ts
0% 100% 0% 0%
๐Ÿ”ด
... / list.ts
0% 100% 0% 0%
๐Ÿ”ด
... / notifications.ts
0% 0% 0% 0%
๐ŸŸข
... / global-context.ts
100% 100% 100% 100%
๐ŸŸก
... / notifications-system.ts
69.89% 63.49% 89.47% 77.92%
Show files with reduced coverage ๐Ÿ”ป
St.:grey_question:
File Statements Branches Functions Lines
๐ŸŸข
... / conf-store.ts
100%
90.48% (-2.21% ๐Ÿ”ป)
100% 100%
๐ŸŸข
... / ConcurrentOutput.tsx
98.39% (-1.61% ๐Ÿ”ป)
90.91% (-4.55% ๐Ÿ”ป)
100%
98.33% (-1.67% ๐Ÿ”ป)
๐ŸŸข
... / base-command.ts
84.81% (+0.39% ๐Ÿ”ผ)
80% (-1.25% ๐Ÿ”ป)
77.27%
85.51% (+0.43% ๐Ÿ”ผ)
๐Ÿ”ด
... / cli.ts
1.39% (-0.02% ๐Ÿ”ป)
0% 0%
1.54% (-0.02% ๐Ÿ”ป)

Test suite run success

1908 tests passing in 870 suites.

Report generated by ๐Ÿงชjest coverage report action from f0942ec887e705d30f9e8acd0a6d76a9211b6ed0

github-actions[bot] avatar Jul 03 '24 07:07 github-actions[bot]

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.

github-actions[bot] avatar Jul 03 '24 08:07 github-actions[bot]

/snapit

isaacroldan avatar Jul 03 '24 10:07 isaacroldan

Could we make this easier to test by making the notification system URL overridable by an environment variable? And maybe a command to clear the notification caches?

nickwesselman avatar Sep 13 '24 18:09 nickwesselman

Looks like there is a formatting issue when the message contains \n, we should either recognize or strip them.

โ•ญโ”€ warning โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                                                                                          โ”‚
โ”‚  Deprecated Shopify CLI version                                                                                                          โ”‚
โ”‚                                                                                                                                          โ”‚
   December 13, 2024. Upgrade to Shopify CLI 3.67 or later.   โ”‚I version on
โ”‚                                                                                                                                          โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

nickwesselman avatar Sep 13 '24 19:09 nickwesselman

/snapit

gonzaloriestra avatar Sep 16 '24 10:09 gonzaloriestra

๐Ÿซฐโœจ 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.

github-actions[bot] avatar Sep 16 '24 10:09 github-actions[bot]

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.

github-actions[bot] avatar Oct 26 '24 03:10 github-actions[bot]

We detected some changes at packages/*/src 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.

github-actions[bot] avatar Nov 04 '24 08:11 github-actions[bot]

/snapit

gonzaloriestra avatar Nov 08 '24 16:11 gonzaloriestra

๐Ÿซฐโœจ 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.

github-actions[bot] avatar Nov 08 '24 16:11 github-actions[bot]

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

github-actions[bot] avatar Nov 08 '24 16:11 github-actions[bot]