From 054916950c7917b3ae3e9e5af61b053afd5367e9 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev <irinchev@me.com> Date: Wed, 6 Nov 2024 10:39:23 +0100 Subject: [PATCH 1/8] feat: add support for fetching a greeting CTA as part of the update flow --- packages/cli-repl/src/cli-repl.ts | 12 +++- packages/cli-repl/src/mongosh-repl.ts | 29 ++++++--- .../src/update-notification-manager.ts | 60 ++++++++++++++++++- packages/types/src/index.ts | 2 + 4 files changed, 90 insertions(+), 13 deletions(-) diff --git a/packages/cli-repl/src/cli-repl.ts b/packages/cli-repl/src/cli-repl.ts index e69350f6dd..a1ec1202cd 100644 --- a/packages/cli-repl/src/cli-repl.ts +++ b/packages/cli-repl/src/cli-repl.ts @@ -414,7 +414,11 @@ export class CliRepl implements MongoshIOProvider { markTime(TimingCategories.DriverSetup, 'completed SP setup'); const initialized = await this.mongoshRepl.initialize( initialServiceProvider, - await this.getMoreRecentMongoshVersion() + { + moreRecentMongoshVersion: await this.getMoreRecentMongoshVersion(), + currentVersionCTA: + await this.updateNotificationManager.getGreetingCTAForCurrentVersion(), + } ); markTime(TimingCategories.REPLInstantiation, 'initialized mongosh repl'); this.injectReplFunctions(); @@ -1264,6 +1268,7 @@ export class CliRepl implements MongoshIOProvider { const updateURL = (await this.getConfig('updateURL')).trim(); if (!updateURL) return; + const { version: currentVersion } = require('../package.json'); const localFilePath = this.shellHomeDirectory.localPath( 'update-metadata.json' ); @@ -1271,14 +1276,17 @@ export class CliRepl implements MongoshIOProvider { this.bus.emit('mongosh:fetching-update-metadata', { updateURL, localFilePath, + currentVersion, }); await this.updateNotificationManager.fetchUpdateMetadata( updateURL, - localFilePath + localFilePath, + currentVersion ); this.bus.emit('mongosh:fetching-update-metadata-complete', { latest: await this.updateNotificationManager.getLatestVersionIfMoreRecent(''), + currentVersion, }); } catch (err: any) { this.bus.emit('mongosh:error', err, 'startup'); diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 8903db2ab4..223e60edaf 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -114,6 +114,11 @@ type MongoshRuntimeState = { console: Console; }; +type GreetingDetails = { + moreRecentMongoshVersion?: string | null; + currentVersionCTA?: { text: string; style: StyleDefinition }[]; +}; + /* Utility, inverse of Readonly<T> */ type Mutable<T> = { -readonly [P in keyof T]: T[P]; @@ -177,7 +182,7 @@ class MongoshNodeRepl implements EvaluationListener { */ async initialize( serviceProvider: ServiceProvider, - moreRecentMongoshVersion?: string | null + greeting: GreetingDetails ): Promise<InitializationToken> { const usePlainVMContext = this.shellCliOptions.jsContext === 'plain-vm'; @@ -221,7 +226,7 @@ class MongoshNodeRepl implements EvaluationListener { (mongodVersion ? mongodVersion + ' ' : '') + `(API Version ${apiVersion})`; } - await this.greet(mongodVersion, moreRecentMongoshVersion); + await this.greet(mongodVersion, greeting); } } @@ -577,10 +582,10 @@ class MongoshNodeRepl implements EvaluationListener { /** * The greeting for the shell, showing server and shell version. */ - async greet( - mongodVersion: string, - moreRecentMongoshVersion?: string | null - ): Promise<void> { + async greet(mongodVersion: string, greeting: GreetingDetails): Promise<void> { + this.output.write('sadfasdfasdfassafsa'); + this.output.write(JSON.stringify({ mongodVersion, greeting }) + '\n'); + if (this.shellCliOptions.quiet) { return; } @@ -593,15 +598,23 @@ class MongoshNodeRepl implements EvaluationListener { 'Using Mongosh', 'mongosh:section-header' )}:\t\t${version}\n`; - if (moreRecentMongoshVersion) { + if (greeting.moreRecentMongoshVersion) { text += `mongosh ${this.clr( - moreRecentMongoshVersion, + greeting.moreRecentMongoshVersion, 'bold' )} is available for download: ${this.clr( 'https://www.mongodb.com/try/download/shell', 'mongosh:uri' )}\n`; } + + if (greeting.currentVersionCTA) { + for (const run of greeting.currentVersionCTA) { + text += this.clr(run.text, run.style); + } + text += '\n'; + } + text += `${MONGOSH_WIKI}\n`; if (!(await this.getConfig('disableGreetingMessage'))) { text += `${TELEMETRY_GREETING_MESSAGE}\n`; diff --git a/packages/cli-repl/src/update-notification-manager.ts b/packages/cli-repl/src/update-notification-manager.ts index 12a7604d3c..87ff1a16f7 100644 --- a/packages/cli-repl/src/update-notification-manager.ts +++ b/packages/cli-repl/src/update-notification-manager.ts @@ -7,18 +7,37 @@ import type { Response, } from '@mongodb-js/devtools-proxy-support'; import { createFetch } from '@mongodb-js/devtools-proxy-support'; +import { StyleDefinition } from './clr'; + +interface GreetingCTADetails { + chunks: { + text: string; + style: StyleDefinition; + }[]; +} + +interface MongoshVersionsContents { + versions: { + version: string; + cta?: GreetingCTADetails; + }[]; +} interface MongoshUpdateLocalFileContents { lastChecked?: number; latestKnownMongoshVersion?: string; etag?: string; updateURL?: string; + cta?: { + [version: string]: GreetingCTADetails | undefined; + }; } // Utility for fetching metadata about potentially available newer versions // and returning that latest version if available. export class UpdateNotificationManager { private latestKnownMongoshVersion: string | undefined = undefined; + private currentVersionGreetingCTA: GreetingCTADetails | undefined = undefined; private localFilesystemFetchInProgress: Promise<unknown> | undefined = undefined; private fetch: (url: string, init: RequestInit) => Promise<Response>; @@ -49,12 +68,29 @@ export class UpdateNotificationManager { return this.latestKnownMongoshVersion; } + async getGreetingCTAForCurrentVersion(): Promise< + | { + text: string; + style: StyleDefinition; + }[] + | undefined + > { + try { + await this.localFilesystemFetchInProgress; + } catch { + /* already handled in fetchUpdateMetadata() */ + } + + return this.currentVersionGreetingCTA?.chunks; + } + // Fetch update metadata, taking into account a local cache and an external // JSON feed. This function will throw in case it failed to load information // about latest versions. async fetchUpdateMetadata( updateURL: string, - localFilePath: string + localFilePath: string, + currentVersion: string ): Promise<void> { let localFileContents: MongoshUpdateLocalFileContents | undefined; await (this.localFilesystemFetchInProgress = (async () => { @@ -90,6 +126,10 @@ export class UpdateNotificationManager { localFileContents.latestKnownMongoshVersion; } + if (localFileContents?.cta && currentVersion in localFileContents.cta) { + this.currentVersionGreetingCTA = localFileContents.cta[currentVersion]; + } + this.localFilesystemFetchInProgress = undefined; })()); @@ -123,17 +163,31 @@ export class UpdateNotificationManager { ); } - const jsonContents = (await response.json()) as { versions?: any[] }; + const jsonContents = (await response.json()) as MongoshVersionsContents; this.latestKnownMongoshVersion = jsonContents?.versions - ?.map((v: any) => v.version as string) + ?.map((v) => v.version) ?.filter((v) => !semver.prerelease(v)) ?.sort(semver.rcompare)?.[0]; + this.currentVersionGreetingCTA = jsonContents?.versions?.filter( + (v) => v.version === currentVersion + )?.[0]?.cta; + + const latestKnownVersionCTA = jsonContents?.versions?.filter( + (v) => v.version === this.latestKnownMongoshVersion + )?.[0]?.cta; + localFileContents = { updateURL, lastChecked: Date.now(), etag: response.headers.get('etag') ?? undefined, latestKnownMongoshVersion: this.latestKnownMongoshVersion, + cta: { + [currentVersion]: this.currentVersionGreetingCTA, + ...(this.latestKnownMongoshVersion && { + [this.latestKnownMongoshVersion]: latestKnownVersionCTA, + }), + }, }; await fs.writeFile(localFilePath, JSON.stringify(localFileContents)); } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 06fa242c0d..39f8190f46 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -173,10 +173,12 @@ export interface EditorReadVscodeExtensionsFailedEvent { export interface FetchingUpdateMetadataEvent { updateURL: string; localFilePath: string; + currentVersion: string; } export interface FetchingUpdateMetadataCompleteEvent { latest: string | null; + currentVersion: string; } export interface SessionStartedEvent { From 43efe22b5cf4fabb6f01b831caa307c77047e233 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev <irinchev@me.com> Date: Wed, 6 Nov 2024 10:55:58 +0100 Subject: [PATCH 2/8] Add a comment --- packages/cli-repl/src/cli-repl.ts | 2 ++ packages/cli-repl/src/update-notification-manager.ts | 10 +++++++--- packages/types/src/index.ts | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/cli-repl/src/cli-repl.ts b/packages/cli-repl/src/cli-repl.ts index a1ec1202cd..c9756ec1ad 100644 --- a/packages/cli-repl/src/cli-repl.ts +++ b/packages/cli-repl/src/cli-repl.ts @@ -1287,6 +1287,8 @@ export class CliRepl implements MongoshIOProvider { latest: await this.updateNotificationManager.getLatestVersionIfMoreRecent(''), currentVersion, + hasGreetingCTA: + !!(await this.updateNotificationManager.getGreetingCTAForCurrentVersion()), }); } catch (err: any) { this.bus.emit('mongosh:error', err, 'startup'); diff --git a/packages/cli-repl/src/update-notification-manager.ts b/packages/cli-repl/src/update-notification-manager.ts index 87ff1a16f7..b459fbc2a6 100644 --- a/packages/cli-repl/src/update-notification-manager.ts +++ b/packages/cli-repl/src/update-notification-manager.ts @@ -184,9 +184,13 @@ export class UpdateNotificationManager { latestKnownMongoshVersion: this.latestKnownMongoshVersion, cta: { [currentVersion]: this.currentVersionGreetingCTA, - ...(this.latestKnownMongoshVersion && { - [this.latestKnownMongoshVersion]: latestKnownVersionCTA, - }), + + // Add the latest known version's CTA if we're not already on latest. This could be used + // next time we start mongosh if the user has updated to latest. + ...(this.latestKnownMongoshVersion && + this.latestKnownMongoshVersion !== currentVersion && { + [this.latestKnownMongoshVersion]: latestKnownVersionCTA, + }), }, }; await fs.writeFile(localFilePath, JSON.stringify(localFileContents)); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 39f8190f46..22da72a78a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -179,6 +179,7 @@ export interface FetchingUpdateMetadataEvent { export interface FetchingUpdateMetadataCompleteEvent { latest: string | null; currentVersion: string; + hasGreetingCTA: boolean; } export interface SessionStartedEvent { From 2a0955f6d3a9407b65eb07ceb278d7057f29885f Mon Sep 17 00:00:00 2001 From: Nikola Irinchev <irinchev@me.com> Date: Fri, 8 Nov 2024 08:45:23 +0100 Subject: [PATCH 3/8] Wire up some basic cli commands for updating the cta --- config/cta.conf.js | 21 +++++ packages/build/src/download-center/config.ts | 87 +++++++++++++++---- .../build/src/download-center/constants.ts | 15 ++-- packages/build/src/index.ts | 63 +++++++++----- packages/cli-repl/src/mongosh-repl.ts | 11 ++- .../src/update-notification-manager.spec.ts | 21 +++-- .../src/update-notification-manager.ts | 14 +-- .../src/snippet-manager.spec.ts | 2 +- 8 files changed, 173 insertions(+), 61 deletions(-) create mode 100644 config/cta.conf.js diff --git a/config/cta.conf.js b/config/cta.conf.js new file mode 100644 index 0000000000..2f65194b89 --- /dev/null +++ b/config/cta.conf.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = { + awsAccessKeyId: process.env.DOWNLOAD_CENTER_AWS_KEY, + awsSecretAccessKey: process.env.DOWNLOAD_CENTER_AWS_SECRET, + ctas: { + // Define the ctas per version here. '*' is the default cta which will be shown if there's no specific cta + // for the current version. + // '*': { + // runs: [ + // { text: 'Example', style: 'bold' }, + // ] + // }, + // '1.2.3': { + // runs: [ + // { text: 'Example', style: 'mongosh:uri' }, + // ] + // } + }, + isDryRun: false, +} \ No newline at end of file diff --git a/packages/build/src/download-center/config.ts b/packages/build/src/download-center/config.ts index b88890037d..d8f3230c71 100644 --- a/packages/build/src/download-center/config.ts +++ b/packages/build/src/download-center/config.ts @@ -9,7 +9,7 @@ import type { } from '@mongodb-js/dl-center/dist/download-center-config'; import { ARTIFACTS_BUCKET, - ARTIFACTS_FOLDER, + JSON_FEED_ARTIFACT_KEY, ARTIFACTS_URL_PUBLIC_BASE, CONFIGURATION_KEY, CONFIGURATIONS_BUCKET, @@ -32,6 +32,24 @@ import path from 'path'; import semver from 'semver'; import { hashListFiles } from '../run-download-and-list-artifacts'; +async function getCurrentJsonFeed( + dlcenterArtifacts: DownloadCenterCls +): Promise<JsonFeed | undefined> { + let existingJsonFeedText; + try { + existingJsonFeedText = await dlcenterArtifacts.downloadAsset( + JSON_FEED_ARTIFACT_KEY + ); + } catch (err: any) { + console.warn('Failed to get existing JSON feed text', err); + if (err?.code !== 'NoSuchKey') throw err; + } + + return existingJsonFeedText + ? JSON.parse(existingJsonFeedText.toString()) + : undefined; +} + export async function createAndPublishDownloadCenterConfig( outputDir: string, packageInformation: PackageInformationProvider, @@ -80,20 +98,8 @@ export async function createAndPublishDownloadCenterConfig( accessKeyId: awsAccessKeyId, secretAccessKey: awsSecretAccessKey, }); - const jsonFeedArtifactkey = `${ARTIFACTS_FOLDER}/mongosh.json`; - let existingJsonFeedText; - try { - existingJsonFeedText = await dlcenterArtifacts.downloadAsset( - jsonFeedArtifactkey - ); - } catch (err: any) { - console.warn('Failed to get existing JSON feed text', err); - if (err?.code !== 'NoSuchKey') throw err; - } - const existingJsonFeed: JsonFeed | undefined = existingJsonFeedText - ? JSON.parse(existingJsonFeedText.toString()) - : undefined; + const existingJsonFeed = await getCurrentJsonFeed(dlcenterArtifacts); const injectedJsonFeed: JsonFeed | undefined = injectedJsonFeedFile ? JSON.parse(await fs.readFile(injectedJsonFeedFile, 'utf8')) : undefined; @@ -122,12 +128,42 @@ export async function createAndPublishDownloadCenterConfig( await Promise.all([ dlcenter.uploadConfig(CONFIGURATION_KEY, config), dlcenterArtifacts.uploadAsset( - jsonFeedArtifactkey, + JSON_FEED_ARTIFACT_KEY, JSON.stringify(newJsonFeed, null, 2) ), ]); } +export async function updateJsonFeedCTA( + config: UpdateCTAConfig, + DownloadCenter: typeof DownloadCenterCls = DownloadCenterCls +) { + const dlcenterArtifacts = new DownloadCenter({ + bucket: ARTIFACTS_BUCKET, + accessKeyId: config.awsAccessKeyId, + secretAccessKey: config.awsSecretAccessKey, + }); + + const jsonFeed = await getCurrentJsonFeed(dlcenterArtifacts); + if (!jsonFeed) { + throw new Error('No existing JSON feed found'); + } + + jsonFeed.cta = config.ctas['*']; + for (const version of jsonFeed.versions) { + version.cta = config.ctas[version.version]; + } + + const patchedJsonFeed = JSON.stringify(jsonFeed, null, 2); + if (config.isDryRun) { + console.warn('Not uploading JSON feed in dry-run mode'); + console.warn(`Patched JSON feed: ${patchedJsonFeed}`); + return; + } + + await dlcenterArtifacts.uploadAsset(JSON_FEED_ARTIFACT_KEY, patchedJsonFeed); +} + export function getUpdatedDownloadCenterConfig( downloadedConfig: DownloadCenterConfig, getVersionConfig: () => ReturnType<typeof createVersionConfig> @@ -201,13 +237,32 @@ export function createVersionConfig( }; } +// TODO: this is duplicated in update-notification-manager.ts +interface GreetingCTADetails { + chunks: { + text: string; + style: string; // TODO: this is actually clr.ts/StyleDefinition + }[]; +} + +export interface UpdateCTAConfig { + ctas: { + [version: string]: GreetingCTADetails; + }; + awsAccessKeyId: string; + awsSecretAccessKey: string; + isDryRun: boolean; +} + interface JsonFeed { versions: JsonFeedVersionEntry[]; + cta?: GreetingCTADetails; } interface JsonFeedVersionEntry { version: string; downloads: JsonFeedDownloadEntry[]; + cta?: GreetingCTADetails; } interface JsonFeedDownloadEntry { @@ -275,6 +330,8 @@ function mergeFeeds(...args: (JsonFeed | undefined)[]): JsonFeed { if (index === -1) newFeed.versions.unshift(version); else newFeed.versions.splice(index, 1, version); } + + newFeed.cta = feed?.cta ?? newFeed.cta; } newFeed.versions.sort((a, b) => semver.rcompare(a.version, b.version)); return newFeed; diff --git a/packages/build/src/download-center/constants.ts b/packages/build/src/download-center/constants.ts index b3d953fd33..5ba6a4d773 100644 --- a/packages/build/src/download-center/constants.ts +++ b/packages/build/src/download-center/constants.ts @@ -3,25 +3,30 @@ const fallback = require('./fallback.json'); /** * The S3 bucket for download center configurations. */ -export const CONFIGURATIONS_BUCKET = 'info-mongodb-com' as const; +export const CONFIGURATIONS_BUCKET = 'info-mongodb-com'; /** * The S3 object key for the download center configuration. */ export const CONFIGURATION_KEY = - 'com-download-center/mongosh.multiversion.json' as const; + 'com-download-center/mongosh.multiversion.json'; /** * The S3 bucket for download center artifacts. */ -export const ARTIFACTS_BUCKET = 'downloads.10gen.com' as const; +export const ARTIFACTS_BUCKET = 'downloads.10gen.com'; /** * The S3 "folder" for uploaded artifacts. */ -export const ARTIFACTS_FOLDER = 'compass' as const; +export const ARTIFACTS_FOLDER = 'compass'; + +/** + * The S3 artifact key for the versions JSON feed. + */ +export const JSON_FEED_ARTIFACT_KEY = `${ARTIFACTS_FOLDER}/mongosh.json`; export const ARTIFACTS_URL_PUBLIC_BASE = - 'https://downloads.mongodb.com/compass/' as const; + 'https://downloads.mongodb.com/compass/'; export const ARTIFACTS_FALLBACK = Object.freeze(fallback); diff --git a/packages/build/src/index.ts b/packages/build/src/index.ts index 064557fa63..1a2e237829 100644 --- a/packages/build/src/index.ts +++ b/packages/build/src/index.ts @@ -6,10 +6,11 @@ import { triggerRelease } from './local'; import type { ReleaseCommand } from './release'; import { release } from './release'; import type { Config, PackageVariant } from './config'; +import { updateJsonFeedCTA, UpdateCTAConfig } from './download-center'; export { getArtifactUrl, downloadMongoDb }; -const validCommands: (ReleaseCommand | 'trigger-release')[] = [ +const validCommands: (ReleaseCommand | 'trigger-release' | 'update-cta')[] = [ 'bump', 'compile', 'package', @@ -20,11 +21,12 @@ const validCommands: (ReleaseCommand | 'trigger-release')[] = [ 'download-crypt-shared-library', 'download-and-list-artifacts', 'trigger-release', + 'update-cta', ] as const; const isValidCommand = ( cmd: string -): cmd is ReleaseCommand | 'trigger-release' => +): cmd is ReleaseCommand | 'trigger-release' | 'update-cta' => (validCommands as string[]).includes(cmd); if (require.main === module) { @@ -38,29 +40,46 @@ if (require.main === module) { ); } - if (command === 'trigger-release') { - await triggerRelease(process.argv.slice(3)); - } else { - const config: Config = require(path.join( - __dirname, - '..', - '..', - '..', - 'config', - 'build.conf.js' - )); + switch (command) { + case 'trigger-release': + await triggerRelease(process.argv.slice(3)); + break; + case 'update-cta': + const ctaConfig: UpdateCTAConfig = require(path.join( + __dirname, + '..', + '..', + '..', + 'config', + 'cta.conf.js' + )); - const cliBuildVariant = process.argv - .map((arg) => /^--build-variant=(.+)$/.exec(arg)) - .filter(Boolean)[0]; - if (cliBuildVariant) { - config.packageVariant = cliBuildVariant[1] as PackageVariant; - validatePackageVariant(config.packageVariant); - } + ctaConfig.isDryRun ||= process.argv.includes('--dry-run'); - config.isDryRun ||= process.argv.includes('--dry-run'); + await updateJsonFeedCTA(ctaConfig); + break; + default: + const config: Config = require(path.join( + __dirname, + '..', + '..', + '..', + 'config', + 'build.conf.js' + )); - await release(command, config); + const cliBuildVariant = process.argv + .map((arg) => /^--build-variant=(.+)$/.exec(arg)) + .filter(Boolean)[0]; + if (cliBuildVariant) { + config.packageVariant = cliBuildVariant[1] as PackageVariant; + validatePackageVariant(config.packageVariant); + } + + config.isDryRun ||= process.argv.includes('--dry-run'); + + await release(command, config); + break; } })().then( () => process.exit(0), diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 223e60edaf..3182100e81 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -182,7 +182,7 @@ class MongoshNodeRepl implements EvaluationListener { */ async initialize( serviceProvider: ServiceProvider, - greeting: GreetingDetails + greeting?: GreetingDetails ): Promise<InitializationToken> { const usePlainVMContext = this.shellCliOptions.jsContext === 'plain-vm'; @@ -582,7 +582,10 @@ class MongoshNodeRepl implements EvaluationListener { /** * The greeting for the shell, showing server and shell version. */ - async greet(mongodVersion: string, greeting: GreetingDetails): Promise<void> { + async greet( + mongodVersion: string, + greeting?: GreetingDetails + ): Promise<void> { this.output.write('sadfasdfasdfassafsa'); this.output.write(JSON.stringify({ mongodVersion, greeting }) + '\n'); @@ -598,7 +601,7 @@ class MongoshNodeRepl implements EvaluationListener { 'Using Mongosh', 'mongosh:section-header' )}:\t\t${version}\n`; - if (greeting.moreRecentMongoshVersion) { + if (greeting?.moreRecentMongoshVersion) { text += `mongosh ${this.clr( greeting.moreRecentMongoshVersion, 'bold' @@ -608,7 +611,7 @@ class MongoshNodeRepl implements EvaluationListener { )}\n`; } - if (greeting.currentVersionCTA) { + if (greeting?.currentVersionCTA) { for (const run of greeting.currentVersionCTA) { text += this.clr(run.text, run.style); } diff --git a/packages/cli-repl/src/update-notification-manager.spec.ts b/packages/cli-repl/src/update-notification-manager.spec.ts index 85faa8f681..2fbffcc5bb 100644 --- a/packages/cli-repl/src/update-notification-manager.spec.ts +++ b/packages/cli-repl/src/update-notification-manager.spec.ts @@ -41,28 +41,33 @@ describe('UpdateNotificationManager', function () { it('fetches and stores information about the current release', async function () { const manager = new UpdateNotificationManager(); - await manager.fetchUpdateMetadata(httpServerUrl, filename); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.2.3'); expect(await manager.getLatestVersionIfMoreRecent('')).to.equal(null); expect(reqHandler).to.have.been.calledOnce; const fileContents = JSON.parse(await fs.readFile(filename, 'utf-8')); expect(Object.keys(fileContents)).to.deep.equal([ 'updateURL', 'lastChecked', + 'cta', ]); expect(fileContents.lastChecked).to.be.a('number'); }); it('uses existing data if some has been fetched recently', async function () { const manager = new UpdateNotificationManager(); - await manager.fetchUpdateMetadata(httpServerUrl, filename); - await manager.fetchUpdateMetadata(httpServerUrl, filename); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.2.3'); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.2.3'); expect(reqHandler).to.have.been.calledOnce; }); it('does not re-use existing data if the updateURL value has changed', async function () { const manager = new UpdateNotificationManager(); - await manager.fetchUpdateMetadata(httpServerUrl, filename); - await manager.fetchUpdateMetadata(httpServerUrl + '/?foo=bar', filename); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.2.3'); + await manager.fetchUpdateMetadata( + httpServerUrl + '/?foo=bar', + filename, + '1.2.3' + ); expect(reqHandler).to.have.been.calledTwice; }); @@ -80,7 +85,7 @@ describe('UpdateNotificationManager', function () { res.end('{}'); }); const manager = new UpdateNotificationManager(); - await manager.fetchUpdateMetadata(httpServerUrl, filename); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.2.3'); await fs.writeFile( filename, JSON.stringify({ @@ -88,7 +93,7 @@ describe('UpdateNotificationManager', function () { lastChecked: 0, }) ); - await manager.fetchUpdateMetadata(httpServerUrl, filename); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.2.3'); expect(reqHandler).to.have.been.calledTwice; expect(cacheHits).to.equal(1); }); @@ -106,7 +111,7 @@ describe('UpdateNotificationManager', function () { ); }); const manager = new UpdateNotificationManager(); - await manager.fetchUpdateMetadata(httpServerUrl, filename); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.2.3'); expect(await manager.getLatestVersionIfMoreRecent('')).to.equal('1.1.0'); expect(await manager.getLatestVersionIfMoreRecent('1.0.0')).to.equal( '1.1.0' diff --git a/packages/cli-repl/src/update-notification-manager.ts b/packages/cli-repl/src/update-notification-manager.ts index b459fbc2a6..bfb7137b6d 100644 --- a/packages/cli-repl/src/update-notification-manager.ts +++ b/packages/cli-repl/src/update-notification-manager.ts @@ -21,6 +21,7 @@ interface MongoshVersionsContents { version: string; cta?: GreetingCTADetails; }[]; + cta?: GreetingCTADetails; } interface MongoshUpdateLocalFileContents { @@ -169,13 +170,14 @@ export class UpdateNotificationManager { ?.filter((v) => !semver.prerelease(v)) ?.sort(semver.rcompare)?.[0]; - this.currentVersionGreetingCTA = jsonContents?.versions?.filter( - (v) => v.version === currentVersion - )?.[0]?.cta; + this.currentVersionGreetingCTA = + jsonContents?.versions?.filter((v) => v.version === currentVersion)?.[0] + ?.cta ?? jsonContents?.cta; - const latestKnownVersionCTA = jsonContents?.versions?.filter( - (v) => v.version === this.latestKnownMongoshVersion - )?.[0]?.cta; + const latestKnownVersionCTA = + jsonContents?.versions?.filter( + (v) => v.version === this.latestKnownMongoshVersion + )?.[0]?.cta ?? jsonContents?.cta; localFileContents = { updateURL, diff --git a/packages/snippet-manager/src/snippet-manager.spec.ts b/packages/snippet-manager/src/snippet-manager.spec.ts index df1ae238e1..83ce7665f6 100644 --- a/packages/snippet-manager/src/snippet-manager.spec.ts +++ b/packages/snippet-manager/src/snippet-manager.spec.ts @@ -341,7 +341,7 @@ describe('SnippetManager', function () { await eventually(async () => { // This can fail when an index fetch is being written while we are removing // the directory; hence, try again. - await fs.rmdir(tmpdir, { recursive: true }); + await fs.rm(tmpdir, { recursive: true }); }); httpServer.close(); }); From 0c607968356ad551d12d003f7eaf27f962ee5a09 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev <irinchev@me.com> Date: Wed, 13 Nov 2024 14:09:15 +0100 Subject: [PATCH 4/8] Add a test --- configs/eslint-config-mongosh/index.js | 1 + packages/cli-repl/src/clr.ts | 2 +- packages/cli-repl/src/mongosh-repl.ts | 2 +- .../src/update-notification-manager.spec.ts | 70 +++++++++++++++++++ .../src/update-notification-manager.ts | 8 +-- 5 files changed, 77 insertions(+), 6 deletions(-) diff --git a/configs/eslint-config-mongosh/index.js b/configs/eslint-config-mongosh/index.js index 71301c52f4..edffe282dc 100644 --- a/configs/eslint-config-mongosh/index.js +++ b/configs/eslint-config-mongosh/index.js @@ -93,6 +93,7 @@ module.exports = { ...common.testRules, ...extraJSRules, ...extraTypescriptRules, + '@typescript-eslint/no-non-null-assertion': 'off', }, }, ], diff --git a/packages/cli-repl/src/clr.ts b/packages/cli-repl/src/clr.ts index 1a97c5e35d..4b383e63ea 100644 --- a/packages/cli-repl/src/clr.ts +++ b/packages/cli-repl/src/clr.ts @@ -14,7 +14,7 @@ export type StyleDefinition = /** Optionally colorize a string, given a set of style definition(s). */ export default function colorize( text: string, - style: StyleDefinition, + style: StyleDefinition | undefined, options: { colors: boolean } ): string { if (options.colors) { diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 3182100e81..d37b966a63 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -116,7 +116,7 @@ type MongoshRuntimeState = { type GreetingDetails = { moreRecentMongoshVersion?: string | null; - currentVersionCTA?: { text: string; style: StyleDefinition }[]; + currentVersionCTA?: { text: string; style?: StyleDefinition }[]; }; /* Utility, inverse of Readonly<T> */ diff --git a/packages/cli-repl/src/update-notification-manager.spec.ts b/packages/cli-repl/src/update-notification-manager.spec.ts index 2fbffcc5bb..5a154171c8 100644 --- a/packages/cli-repl/src/update-notification-manager.spec.ts +++ b/packages/cli-repl/src/update-notification-manager.spec.ts @@ -7,6 +7,7 @@ import type { AddressInfo } from 'net'; import os from 'os'; import path from 'path'; import { UpdateNotificationManager } from './update-notification-manager'; +import type { MongoshVersionsContents } from './update-notification-manager'; import sinon from 'sinon'; describe('UpdateNotificationManager', function () { @@ -122,4 +123,73 @@ describe('UpdateNotificationManager', function () { await manager.getLatestVersionIfMoreRecent('1.0.0-alpha.0') ).to.equal(null); }); + + it('figures out the greeting CTA when set on a global level', async function () { + const response: MongoshVersionsContents = { + versions: [ + { version: '1.0.0' }, + { + version: '1.1.0', + cta: { chunks: [{ text: "Don't use 1.1.0, downgrade!!" }] }, + }, + ], + cta: { + chunks: [{ text: 'Vote for your favorite feature!', style: 'bold' }], + }, + }; + reqHandler.callsFake((req, res) => { + res.end(JSON.stringify(response)); + }); + + const manager = new UpdateNotificationManager(); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.0.0'); + + const cta = await manager.getGreetingCTAForCurrentVersion(); + expect(cta).to.not.be.undefined; + expect(cta?.length).to.equal(1); + expect(cta![0]?.text).to.equal('Vote for your favorite feature!'); + expect(cta![0]?.style).to.equal('bold'); + }); + + it('figures out the greeting CTA when set on a per-version basis', async function () { + const response: MongoshVersionsContents = { + versions: [ + { + version: '1.0.0', + cta: { + chunks: [ + { text: "Don't use 1.0.0, upgrade!! " }, + { + text: 'https://downloads.mongodb.com/mongosh/1.1.0/', + style: 'mongosh:uri', + }, + ], + }, + }, + { + version: '1.1.0', + cta: { chunks: [{ text: 'This version is very safe!' }] }, + }, + ], + cta: { + chunks: [{ text: 'Vote for your favorite feature!', style: 'bold' }], + }, + }; + reqHandler.callsFake((req, res) => { + res.end(JSON.stringify(response)); + }); + + const manager = new UpdateNotificationManager(); + await manager.fetchUpdateMetadata(httpServerUrl, filename, '1.0.0'); + + const cta = await manager.getGreetingCTAForCurrentVersion(); + expect(cta).to.not.be.undefined; + expect(cta?.length).to.equal(2); + expect(cta![0]?.text).to.equal("Don't use 1.0.0, upgrade!! "); + expect(cta![0]?.style).to.be.undefined; + expect(cta![1]?.text).to.equal( + 'https://downloads.mongodb.com/mongosh/1.1.0/' + ); + expect(cta![1]?.style).to.equal('mongosh:uri'); + }); }); diff --git a/packages/cli-repl/src/update-notification-manager.ts b/packages/cli-repl/src/update-notification-manager.ts index bfb7137b6d..374fdce408 100644 --- a/packages/cli-repl/src/update-notification-manager.ts +++ b/packages/cli-repl/src/update-notification-manager.ts @@ -7,16 +7,16 @@ import type { Response, } from '@mongodb-js/devtools-proxy-support'; import { createFetch } from '@mongodb-js/devtools-proxy-support'; -import { StyleDefinition } from './clr'; +import type { StyleDefinition } from './clr'; interface GreetingCTADetails { chunks: { text: string; - style: StyleDefinition; + style?: StyleDefinition; }[]; } -interface MongoshVersionsContents { +export interface MongoshVersionsContents { versions: { version: string; cta?: GreetingCTADetails; @@ -72,7 +72,7 @@ export class UpdateNotificationManager { async getGreetingCTAForCurrentVersion(): Promise< | { text: string; - style: StyleDefinition; + style?: StyleDefinition; }[] | undefined > { From 156a2cf835d2018e9a984494eb83c1cbe89f7232 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev <irinchev@me.com> Date: Mon, 2 Dec 2024 11:28:42 +0100 Subject: [PATCH 5/8] Add more tests, github workflow --- .github/workflows/update-cta.yml | 46 ++++ config/cta.conf.js | 6 +- package.json | 1 + packages/build/package.json | 3 +- .../build/src/download-center/config.spec.ts | 224 ++++++++++++++++++ packages/build/src/download-center/config.ts | 6 +- .../build/test/fixtures/cta-versions.json | 33 +++ packages/cli-repl/src/mongosh-repl.ts | 3 - 8 files changed, 312 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/update-cta.yml create mode 100644 packages/build/test/fixtures/cta-versions.json diff --git a/.github/workflows/update-cta.yml b/.github/workflows/update-cta.yml new file mode 100644 index 0000000000..859d40de4f --- /dev/null +++ b/.github/workflows/update-cta.yml @@ -0,0 +1,46 @@ +name: Update greeting CTA +on: + push: + branches: + - main + paths: + - config/cta.conf.js + workflow_dispatch: + inputs: + dry-run: + description: Run the script without updating the CTA + type: boolean + required: false + default: false + environment: + description: The environment to run the script in - must have the DOWNLOAD_CENTER_AWS_KEY and DOWNLOAD_CENTER_AWS_SECRET secrets configured + type: environment + required: true + default: CTA-Production + +jobs: + dry-run: + name: Update greeting CTA + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment || 'CTA-Production'}} + env: + npm_config_loglevel: verbose + npm_config_foreground_scripts: "true" + PUPPETEER_SKIP_DOWNLOAD: "true" + DOWNLOAD_CENTER_AWS_KEY: ${{ secrets.DOWNLOAD_CENTER_AWS_KEY }} + DOWNLOAD_CENTER_AWS_SECRET: ${{ secrets.DOWNLOAD_CENTER_AWS_SECRET }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ^20.x + cache: "npm" + + - name: Install Dependencies and Compile + run: | + npm ci + npm run compile + + - name: Update greeting CTA + run: | + npm run update-cta ${{ github.event.inputs.dry-run && '-- --dry-run' || '' }} diff --git a/config/cta.conf.js b/config/cta.conf.js index 2f65194b89..f1dc72b761 100644 --- a/config/cta.conf.js +++ b/config/cta.conf.js @@ -7,15 +7,15 @@ module.exports = { // Define the ctas per version here. '*' is the default cta which will be shown if there's no specific cta // for the current version. // '*': { - // runs: [ + // chunks: [ // { text: 'Example', style: 'bold' }, // ] // }, // '1.2.3': { - // runs: [ + // chunks: [ // { text: 'Example', style: 'mongosh:uri' }, // ] // } }, isDryRun: false, -} \ No newline at end of file +} diff --git a/package.json b/package.json index 90611bdf32..8051f48cd4 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "compile-all": "npm run compile-compass && npm run compile-exec", "evergreen-release": "cd packages/build && npm run evergreen-release --", "release": "cd packages/build && npm run release --", + "update-cta": "cd packages/build && npm run update-cta --", "report-missing-help": "npm run report-missing-help --workspace @mongosh/shell-api", "report-supported-api": "npm run report-supported-api --workspace @mongosh/shell-api", "post-process-nyc": "ts-node scripts/nyc/post-process-nyc-output.ts", diff --git a/packages/build/package.json b/packages/build/package.json index f96bf7c568..6077194abe 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -26,7 +26,8 @@ "evergreen-release": "ts-node -r ../../scripts/import-expansions.js src/index.ts", "release": "ts-node src/index.ts trigger-release", "prettier": "prettier", - "reformat": "npm run prettier -- --write . && npm run eslint --fix" + "reformat": "npm run prettier -- --write . && npm run eslint --fix", + "update-cta": "ts-node src/index.ts update-cta" }, "license": "Apache-2.0", "publishConfig": { diff --git a/packages/build/src/download-center/config.spec.ts b/packages/build/src/download-center/config.spec.ts index e0decdf9d4..31abb7148d 100644 --- a/packages/build/src/download-center/config.spec.ts +++ b/packages/build/src/download-center/config.spec.ts @@ -10,6 +10,9 @@ import { getUpdatedDownloadCenterConfig, createAndPublishDownloadCenterConfig, createJsonFeedEntry, + updateJsonFeedCTA, + UpdateCTAConfig, + JsonFeed, } from './config'; import { promises as fs } from 'fs'; import path from 'path'; @@ -529,4 +532,225 @@ describe('DownloadCenter config', function () { expect(serverTargets).to.include(target); }); }); + + describe('updateJsonFeedCTA', function () { + let dlCenter: any; + let uploadConfig: sinon.SinonStub; + let downloadConfig: sinon.SinonStub; + let uploadAsset: sinon.SinonStub; + let downloadAsset: sinon.SinonStub; + + const existingUploadedJsonFeed = require(path.resolve( + __dirname, + '..', + '..', + 'test', + 'fixtures', + 'cta-versions.json' + )) as JsonFeed; + + const getConfig = (ctas: UpdateCTAConfig['ctas']): UpdateCTAConfig => { + return { + ctas, + isDryRun: false, + awsAccessKeyId: 'accessKey', + awsSecretAccessKey: 'secretKey', + }; + }; + + const getUploadedJsonFeed = (): JsonFeed => { + return JSON.parse(uploadAsset.lastCall.args[1]) as JsonFeed; + }; + + beforeEach(function () { + uploadConfig = sinon.stub(); + downloadConfig = sinon.stub(); + uploadAsset = sinon.stub(); + downloadAsset = sinon.stub(); + dlCenter = sinon.stub(); + + downloadAsset.returns(JSON.stringify(existingUploadedJsonFeed)); + + dlCenter.returns({ + downloadConfig, + uploadConfig, + uploadAsset, + downloadAsset, + }); + }); + + for (let dryRun of [false, true]) { + it(`when dryRun is ${dryRun}, does ${ + dryRun ? 'not ' : '' + }upload the updated json feed`, async function () { + const config = getConfig({ + '1.10.3': { + chunks: [{ text: 'Foo' }], + }, + '*': { + chunks: [{ text: 'Bar' }], + }, + }); + + config.isDryRun = dryRun; + + await updateJsonFeedCTA(config, dlCenter); + if (dryRun) { + expect(uploadAsset).to.not.have.been.called; + } else { + expect(uploadAsset).to.have.been.called; + + const updatedJsonFeed = getUploadedJsonFeed(); + expect(updatedJsonFeed.cta?.chunks).to.deep.equal([{ text: 'Bar' }]); + expect( + updatedJsonFeed.versions.filter((v) => v.version === '1.10.3')[0] + .cta?.chunks + ).to.deep.equal([{ text: 'Foo' }]); + expect( + updatedJsonFeed.versions.filter((v) => v.version === '1.10.4')[0] + .cta + ).to.be.undefined; + } + }); + } + + it('cannot add new versions', async function () { + expect( + existingUploadedJsonFeed.versions.filter((v) => v.version === '1.10.5') + ).to.have.lengthOf(0); + + const config = getConfig({ + '1.10.5': { + chunks: [{ text: 'Foo' }], + }, + }); + + await updateJsonFeedCTA(config, dlCenter); + + const updatedJsonFeed = getUploadedJsonFeed(); + + expect( + updatedJsonFeed.versions.filter((v) => v.version === '1.10.5') + ).to.have.lengthOf(0); + }); + + it('can remove global cta', async function () { + // Preserve existing CTAs, but omit the global one + const ctas = (existingUploadedJsonFeed.versions as any[]).reduce( + (acc, current) => { + acc[current.version] = current.cta; + return acc; + }, + {} + ); + const config = getConfig(ctas); + + expect(config.ctas['*']).to.be.undefined; + await updateJsonFeedCTA(config, dlCenter); + + const updatedJsonFeed = getUploadedJsonFeed(); + + expect(updatedJsonFeed.cta).to.be.undefined; + }); + + it('can remove version specific cta', async function () { + expect( + existingUploadedJsonFeed.versions.map((v) => v.cta).filter((cta) => cta) + ).to.have.length.greaterThan(0); + + const config = getConfig({ + '*': existingUploadedJsonFeed.cta!, + }); + + await updateJsonFeedCTA(config, dlCenter); + + const updatedJsonFeed = getUploadedJsonFeed(); + expect(updatedJsonFeed.cta).to.not.be.undefined; + expect( + updatedJsonFeed.versions.map((v) => v.cta).filter((cta) => cta) + ).to.have.lengthOf(0); + }); + + it('can update global cta', async function () { + const config = getConfig({ + '*': { + chunks: [{ text: "It's a beautiful day", style: 'imagePositive' }], + }, + }); + + await updateJsonFeedCTA(config, dlCenter); + + const updatedJsonFeed = getUploadedJsonFeed(); + + expect(updatedJsonFeed.cta).to.deep.equal({ + chunks: [{ text: "It's a beautiful day", style: 'imagePositive' }], + }); + }); + + it('can update version-specific cta', async function () { + const config = getConfig({ + '1.10.3': { + chunks: [{ text: "It's a beautiful day", style: 'imagePositive' }], + }, + }); + + await updateJsonFeedCTA(config, dlCenter); + + const updatedJsonFeed = getUploadedJsonFeed(); + + expect( + updatedJsonFeed.versions.filter((v) => v.version === '1.10.3')[0].cta + ).to.deep.equal({ + chunks: [{ text: "It's a beautiful day", style: 'imagePositive' }], + }); + }); + + it('can add global cta', async function () { + // Remove the existing cta + existingUploadedJsonFeed.cta = undefined; + + const config = getConfig({ + '*': { + chunks: [ + { text: 'Go outside and enjoy the sun', style: 'imagePositive' }, + ], + }, + }); + + await updateJsonFeedCTA(config, dlCenter); + + const updatedJsonFeed = getUploadedJsonFeed(); + + expect(updatedJsonFeed.cta).to.deep.equal({ + chunks: [ + { text: 'Go outside and enjoy the sun', style: 'imagePositive' }, + ], + }); + }); + + it('can add version-specific cta', async function () { + // Remove the existing cta + existingUploadedJsonFeed.cta = undefined; + + const config = getConfig({ + '1.10.4': { + chunks: [ + { text: 'Go outside and enjoy the sun', style: 'imagePositive' }, + ], + }, + }); + + await updateJsonFeedCTA(config, dlCenter); + + const updatedJsonFeed = getUploadedJsonFeed(); + + expect( + updatedJsonFeed.versions.filter((v) => v.version === '1.10.4')[0].cta + ).to.deep.equal({ + chunks: [ + { text: 'Go outside and enjoy the sun', style: 'imagePositive' }, + ], + }); + }); + }); }); diff --git a/packages/build/src/download-center/config.ts b/packages/build/src/download-center/config.ts index d8f3230c71..02d26b5483 100644 --- a/packages/build/src/download-center/config.ts +++ b/packages/build/src/download-center/config.ts @@ -241,20 +241,20 @@ export function createVersionConfig( interface GreetingCTADetails { chunks: { text: string; - style: string; // TODO: this is actually clr.ts/StyleDefinition + style?: string; // TODO: this is actually clr.ts/StyleDefinition }[]; } export interface UpdateCTAConfig { ctas: { - [version: string]: GreetingCTADetails; + [version: string | '*']: GreetingCTADetails; }; awsAccessKeyId: string; awsSecretAccessKey: string; isDryRun: boolean; } -interface JsonFeed { +export interface JsonFeed { versions: JsonFeedVersionEntry[]; cta?: GreetingCTADetails; } diff --git a/packages/build/test/fixtures/cta-versions.json b/packages/build/test/fixtures/cta-versions.json new file mode 100644 index 0000000000..f73ad3bd36 --- /dev/null +++ b/packages/build/test/fixtures/cta-versions.json @@ -0,0 +1,33 @@ +{ + "versions": [ + { + "version": "1.10.3", + "cta": { + "chunks": [ + { + "text": "Critical update available: 1.10.4 ", + "style": "bold" + }, + { + "text": "https://www.mongodb.com/try/download/shell", + "style": "mongosh:uri" + } + ] + } + }, + { + "version": "1.10.4" + } + ], + "cta": { + "chunks": [ + { + "text": "Vote for your favorite feature in mongosh " + }, + { + "text": "https://mongodb.com/surveys/shell/2024-Q4", + "style": "mongosh:uri" + } + ] + } +} diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index d37b966a63..6efb394479 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -586,9 +586,6 @@ class MongoshNodeRepl implements EvaluationListener { mongodVersion: string, greeting?: GreetingDetails ): Promise<void> { - this.output.write('sadfasdfasdfassafsa'); - this.output.write(JSON.stringify({ mongodVersion, greeting }) + '\n'); - if (this.shellCliOptions.quiet) { return; } From b1fb4f8b0ea9e7ce45dfe7da46a7af6135c92556 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev <irinchev@me.com> Date: Thu, 6 Feb 2025 15:12:46 +0100 Subject: [PATCH 6/8] Rework cta validations --- config/build.conf.js | 6 + config/cta-config.json | 3 + config/cta-config.schema.json | 94 +++++++++++ config/cta.conf.js | 21 --- package-lock.json | 23 +++ packages/build/package.json | 1 + packages/build/src/config/config.ts | 15 ++ .../build/src/download-center/config.spec.ts | 156 +++++++++++------- packages/build/src/download-center/config.ts | 45 +++-- packages/build/src/index.ts | 79 +++++---- packages/build/src/publish-mongosh.ts | 3 +- packages/build/test/helpers.ts | 2 + 12 files changed, 312 insertions(+), 136 deletions(-) create mode 100644 config/cta-config.json create mode 100644 config/cta-config.schema.json delete mode 100644 config/cta.conf.js diff --git a/config/build.conf.js b/config/build.conf.js index 75b1435091..a45ae8bf0b 100644 --- a/config/build.conf.js +++ b/config/build.conf.js @@ -75,6 +75,10 @@ const MANPAGE_NAME = 'mongosh.1.gz' */ const PACKAGE_VARIANT = process.env.PACKAGE_VARIANT; +const CTA_CONFIG = require(path.join(ROOT, 'config', 'cta-config.json')); + +const CTA_CONFIG_SCHEMA = require(path.join(ROOT, 'config', 'cta-config.schema.json')); + /** * Export the configuration for the build. */ @@ -194,4 +198,6 @@ module.exports = { downloadPath: path.resolve(TMP_DIR, 'manpage'), fileName: MANPAGE_NAME, }, + ctaConfig: CTA_CONFIG, + ctaConfigSchema: CTA_CONFIG_SCHEMA, }; diff --git a/config/cta-config.json b/config/cta-config.json new file mode 100644 index 0000000000..885a4b7f24 --- /dev/null +++ b/config/cta-config.json @@ -0,0 +1,3 @@ +{ + "$schema": "./cta-config.schema.json" +} diff --git a/config/cta-config.schema.json b/config/cta-config.schema.json new file mode 100644 index 0000000000..778ca8aaf2 --- /dev/null +++ b/config/cta-config.schema.json @@ -0,0 +1,94 @@ +{ + "$id": "https://mongodb.com/schemas/mongosh/cta-config", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CTAConfig", + "type": "object", + "properties": { + "*": { + "$ref": "#/definitions/GreetingCTADetails", + "description": "The default CTA for all versions that don't have an explicit one defined." + }, + "$schema": { + "type": "string" + } + }, + "patternProperties": { + "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$": { + "$ref": "#/definitions/GreetingCTADetails", + "description": "The CTA for a specific version.", + "$comment": "The property name must be a valid semver string." + } + }, + "additionalProperties": false, + "definitions": { + "GreetingCTADetails": { + "type": "object", + "additionalProperties": false, + "properties": { + "chunks": { + "description": "The chunks that make up the CTA. They will be combined sequentially with no additional spacing added.", + "items": { + "properties": { + "style": { + "description": "The style to apply to the text. It must match the values from clr.ts/StyleDefinition.", + "enum": [ + "reset", + "bold", + "italic", + "underline", + "fontDefault", + "font2", + "font3", + "font4", + "font5", + "font6", + "imageNegative", + "imagePositive", + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "grey", + "gray", + "bg-black", + "bg-red", + "bg-green", + "bg-yellow", + "bg-blue", + "bg-magenta", + "bg-cyan", + "bg-white", + "bg-grey", + "bg-gray", + "mongosh:warning", + "mongosh:error", + "mongosh:section-header", + "mongosh:uri", + "mongosh:filename", + "mongosh:additional-error-info" + ], + "type": "string" + }, + "text": { + "type": "string", + "description": "The text in the chunk." + } + }, + "type": "object", + "required": [ + "text" + ] + }, + "type": "array" + } + }, + "required": [ + "chunks" + ] + } + } +} diff --git a/config/cta.conf.js b/config/cta.conf.js deleted file mode 100644 index f1dc72b761..0000000000 --- a/config/cta.conf.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -module.exports = { - awsAccessKeyId: process.env.DOWNLOAD_CENTER_AWS_KEY, - awsSecretAccessKey: process.env.DOWNLOAD_CENTER_AWS_SECRET, - ctas: { - // Define the ctas per version here. '*' is the default cta which will be shown if there's no specific cta - // for the current version. - // '*': { - // chunks: [ - // { text: 'Example', style: 'bold' }, - // ] - // }, - // '1.2.3': { - // chunks: [ - // { text: 'Example', style: 'mongosh:uri' }, - // ] - // } - }, - isDryRun: false, -} diff --git a/package-lock.json b/package-lock.json index 3d4bca0a8a..244c238226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29158,6 +29158,7 @@ "@mongodb-js/monorepo-tools": "^1.1.16", "@mongodb-js/signing-utils": "^0.3.7", "@octokit/rest": "^17.9.0", + "ajv": "^8.17.1", "aws-sdk": "^2.674.0", "boxednode": "^2.4.3", "command-exists": "^1.2.9", @@ -29226,6 +29227,28 @@ "@types/node": "*" } }, + "packages/build/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/build/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "packages/build/node_modules/node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", diff --git a/packages/build/package.json b/packages/build/package.json index 1438cb14f4..cf1a56c207 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -70,6 +70,7 @@ "@mongodb-js/monorepo-tools": "^1.1.16", "@mongodb-js/signing-utils": "^0.3.7", "@octokit/rest": "^17.9.0", + "ajv": "^8.17.1", "aws-sdk": "^2.674.0", "boxednode": "^2.4.3", "command-exists": "^1.2.9", diff --git a/packages/build/src/config/config.ts b/packages/build/src/config/config.ts index 47d376aa9b..0c2676f0a1 100644 --- a/packages/build/src/config/config.ts +++ b/packages/build/src/config/config.ts @@ -1,3 +1,4 @@ +import { Schema } from 'ajv'; import type { PackageInformationProvider } from '../packaging/package'; import type { PackageVariant } from './build-variant'; @@ -7,6 +8,18 @@ interface ManPageConfig { fileName: string; } +// TODO: this is duplicated in update-notification-manager.ts +export interface GreetingCTADetails { + chunks: { + text: string; + style?: string; // TODO: this is actually clr.ts/StyleDefinition + }[]; +} + +export type CTAConfig = { + [version: string | '*']: GreetingCTADetails; +}; + /** * Defines the configuration interface for the build system. */ @@ -47,4 +60,6 @@ export interface Config { manpage?: ManPageConfig; isDryRun?: boolean; useAuxiliaryPackagesOnly?: boolean; + ctaConfig: CTAConfig; + ctaConfigSchema: Schema; } diff --git a/packages/build/src/download-center/config.spec.ts b/packages/build/src/download-center/config.spec.ts index 73c412261d..f9dee525cd 100644 --- a/packages/build/src/download-center/config.spec.ts +++ b/packages/build/src/download-center/config.spec.ts @@ -2,7 +2,7 @@ import type { DownloadCenterConfig } from '@mongodb-js/dl-center/dist/download-c import { type PackageInformationProvider } from '../packaging'; import { expect } from 'chai'; import sinon from 'sinon'; -import type { Config } from '../config'; +import type { Config, CTAConfig } from '../config'; import { type PackageVariant } from '../config'; import { createVersionConfig, @@ -12,7 +12,7 @@ import { createJsonFeedEntry, updateJsonFeedCTA, } from './config'; -import type { UpdateCTAConfig, JsonFeed } from './config'; +import type { JsonFeed } from './config'; import { promises as fs } from 'fs'; import path from 'path'; import fetch from 'node-fetch'; @@ -38,6 +38,10 @@ const packageInformation = (version: string) => }; }) as PackageInformationProvider; +const DUMMY_ACCESS_KEY = 'accessKey'; +const DUMMY_SECRET_KEY = 'secretKey'; +const DUMMY_CTA_CONFIG: CTAConfig = {}; + describe('DownloadCenter config', function () { let outputDir: string; before(async function () { @@ -267,23 +271,24 @@ describe('DownloadCenter config', function () { await createAndPublishDownloadCenterConfig( outputDir, packageInformation('2.0.1'), - 'accessKey', - 'secretKey', + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, '', false, + DUMMY_CTA_CONFIG, dlCenter as any, baseUrl ); expect(dlCenter).to.have.been.calledWith({ bucket: 'info-mongodb-com', - accessKeyId: 'accessKey', - secretAccessKey: 'secretKey', + accessKeyId: DUMMY_ACCESS_KEY, + secretAccessKey: DUMMY_SECRET_KEY, }); expect(dlCenter).to.have.been.calledWith({ bucket: 'downloads.10gen.com', - accessKeyId: 'accessKey', - secretAccessKey: 'secretKey', + accessKeyId: DUMMY_ACCESS_KEY, + secretAccessKey: DUMMY_SECRET_KEY, }); expect(uploadConfig).to.be.calledOnce; @@ -325,23 +330,24 @@ describe('DownloadCenter config', function () { await createAndPublishDownloadCenterConfig( outputDir, packageInformation('1.2.2'), - 'accessKey', - 'secretKey', + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, '', false, + DUMMY_CTA_CONFIG, dlCenter as any, baseUrl ); expect(dlCenter).to.have.been.calledWith({ bucket: 'info-mongodb-com', - accessKeyId: 'accessKey', - secretAccessKey: 'secretKey', + accessKeyId: DUMMY_ACCESS_KEY, + secretAccessKey: DUMMY_SECRET_KEY, }); expect(dlCenter).to.have.been.calledWith({ bucket: 'downloads.10gen.com', - accessKeyId: 'accessKey', - secretAccessKey: 'secretKey', + accessKeyId: DUMMY_ACCESS_KEY, + secretAccessKey: DUMMY_SECRET_KEY, }); expect(uploadConfig).to.be.calledOnce; @@ -423,8 +429,8 @@ describe('DownloadCenter config', function () { await createAndPublishDownloadCenterConfig( outputDir, packageInformation('2.0.0'), - 'accessKey', - 'secretKey', + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, path.resolve( __dirname, '..', @@ -434,19 +440,20 @@ describe('DownloadCenter config', function () { 'mongosh-versions.json' ), false, + DUMMY_CTA_CONFIG, dlCenter as any, baseUrl ); expect(dlCenter).to.have.been.calledWith({ bucket: 'info-mongodb-com', - accessKeyId: 'accessKey', - secretAccessKey: 'secretKey', + accessKeyId: DUMMY_ACCESS_KEY, + secretAccessKey: DUMMY_SECRET_KEY, }); expect(dlCenter).to.have.been.calledWith({ bucket: 'downloads.10gen.com', - accessKeyId: 'accessKey', - secretAccessKey: 'secretKey', + accessKeyId: DUMMY_ACCESS_KEY, + secretAccessKey: DUMMY_SECRET_KEY, }); expect(uploadConfig).to.be.calledOnce; @@ -533,7 +540,7 @@ describe('DownloadCenter config', function () { }); describe('updateJsonFeedCTA', function () { - let dlCenter: sinon.SinonStub | DownloadCenterConfig; + let dlCenter: sinon.SinonStub; let uploadConfig: sinon.SinonStub; let downloadConfig: sinon.SinonStub; let uploadAsset: sinon.SinonStub; @@ -548,15 +555,6 @@ describe('DownloadCenter config', function () { 'cta-versions.json' )) as JsonFeed; - const getConfig = (ctas: UpdateCTAConfig['ctas']): UpdateCTAConfig => { - return { - ctas, - isDryRun: false, - awsAccessKeyId: 'accessKey', - awsSecretAccessKey: 'secretKey', - }; - }; - const getUploadedJsonFeed = (): JsonFeed => { return JSON.parse(uploadAsset.lastCall.args[1]) as JsonFeed; }; @@ -582,18 +580,22 @@ describe('DownloadCenter config', function () { it(`when dryRun is ${dryRun}, does ${ dryRun ? 'not ' : '' }upload the updated json feed`, async function () { - const config = getConfig({ + const config: CTAConfig = { '1.10.3': { chunks: [{ text: 'Foo' }], }, '*': { chunks: [{ text: 'Bar' }], }, - }); - - config.isDryRun = dryRun; + }; - await updateJsonFeedCTA(config, dlCenter); + await updateJsonFeedCTA( + config, + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, + dryRun, + dlCenter as any + ); if (dryRun) { expect(uploadAsset).to.not.have.been.called; } else { @@ -618,13 +620,19 @@ describe('DownloadCenter config', function () { existingUploadedJsonFeed.versions.filter((v) => v.version === '1.10.5') ).to.have.lengthOf(0); - const config = getConfig({ + const config: CTAConfig = { '1.10.5': { chunks: [{ text: 'Foo' }], }, - }); + }; - await updateJsonFeedCTA(config, dlCenter); + await updateJsonFeedCTA( + config, + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, + false, + dlCenter as any + ); const updatedJsonFeed = getUploadedJsonFeed(); @@ -642,10 +650,14 @@ describe('DownloadCenter config', function () { }, {} ); - const config = getConfig(ctas); - - expect(config.ctas['*']).to.be.undefined; - await updateJsonFeedCTA(config, dlCenter); + expect(ctas['*']).to.be.undefined; + await updateJsonFeedCTA( + ctas, + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, + false, + dlCenter as any + ); const updatedJsonFeed = getUploadedJsonFeed(); @@ -657,11 +669,17 @@ describe('DownloadCenter config', function () { existingUploadedJsonFeed.versions.map((v) => v.cta).filter((cta) => cta) ).to.have.length.greaterThan(0); - const config = getConfig({ + const config = { '*': existingUploadedJsonFeed.cta!, - }); + }; - await updateJsonFeedCTA(config, dlCenter); + await updateJsonFeedCTA( + config, + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, + false, + dlCenter as any + ); const updatedJsonFeed = getUploadedJsonFeed(); expect(updatedJsonFeed.cta).to.not.be.undefined; @@ -671,13 +689,19 @@ describe('DownloadCenter config', function () { }); it('can update global cta', async function () { - const config = getConfig({ + const config = { '*': { chunks: [{ text: "It's a beautiful day", style: 'imagePositive' }], }, - }); + }; - await updateJsonFeedCTA(config, dlCenter); + await updateJsonFeedCTA( + config, + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, + false, + dlCenter as any + ); const updatedJsonFeed = getUploadedJsonFeed(); @@ -687,13 +711,19 @@ describe('DownloadCenter config', function () { }); it('can update version-specific cta', async function () { - const config = getConfig({ + const config = { '1.10.3': { chunks: [{ text: "It's a beautiful day", style: 'imagePositive' }], }, - }); + }; - await updateJsonFeedCTA(config, dlCenter); + await updateJsonFeedCTA( + config, + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, + false, + dlCenter as any + ); const updatedJsonFeed = getUploadedJsonFeed(); @@ -708,15 +738,21 @@ describe('DownloadCenter config', function () { // Remove the existing cta existingUploadedJsonFeed.cta = undefined; - const config = getConfig({ + const config = { '*': { chunks: [ { text: 'Go outside and enjoy the sun', style: 'imagePositive' }, ], }, - }); + }; - await updateJsonFeedCTA(config, dlCenter); + await updateJsonFeedCTA( + config, + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, + false, + dlCenter as any + ); const updatedJsonFeed = getUploadedJsonFeed(); @@ -731,15 +767,21 @@ describe('DownloadCenter config', function () { // Remove the existing cta existingUploadedJsonFeed.cta = undefined; - const config = getConfig({ + const config = { '1.10.4': { chunks: [ { text: 'Go outside and enjoy the sun', style: 'imagePositive' }, ], }, - }); + }; - await updateJsonFeedCTA(config, dlCenter); + await updateJsonFeedCTA( + config, + DUMMY_ACCESS_KEY, + DUMMY_SECRET_KEY, + false, + dlCenter as any + ); const updatedJsonFeed = getUploadedJsonFeed(); diff --git a/packages/build/src/download-center/config.ts b/packages/build/src/download-center/config.ts index 02d26b5483..09e0d75481 100644 --- a/packages/build/src/download-center/config.ts +++ b/packages/build/src/download-center/config.ts @@ -15,7 +15,7 @@ import { CONFIGURATIONS_BUCKET, ARTIFACTS_FALLBACK, } from './constants'; -import type { PackageVariant } from '../config'; +import type { CTAConfig, GreetingCTADetails, PackageVariant } from '../config'; import { ALL_PACKAGE_VARIANTS, getDownloadCenterDistroDescription, @@ -57,6 +57,7 @@ export async function createAndPublishDownloadCenterConfig( awsSecretAccessKey: string, injectedJsonFeedFile: string, isDryRun: boolean, + ctaConfig: CTAConfig, DownloadCenter: typeof DownloadCenterCls = DownloadCenterCls, publicArtifactBaseUrl: string = ARTIFACTS_URL_PUBLIC_BASE ): Promise<void> { @@ -120,6 +121,8 @@ export async function createAndPublishDownloadCenterConfig( currentJsonFeedWrapped ); + populateJsonFeedCTAs(newJsonFeed, ctaConfig); + if (isDryRun) { console.warn('Not uploading download center config in dry-run mode'); return; @@ -135,13 +138,16 @@ export async function createAndPublishDownloadCenterConfig( } export async function updateJsonFeedCTA( - config: UpdateCTAConfig, + config: CTAConfig, + awsAccessKeyId: string, + awsSecretAccessKey: string, + isDryRun: boolean, DownloadCenter: typeof DownloadCenterCls = DownloadCenterCls ) { const dlcenterArtifacts = new DownloadCenter({ bucket: ARTIFACTS_BUCKET, - accessKeyId: config.awsAccessKeyId, - secretAccessKey: config.awsSecretAccessKey, + accessKeyId: awsAccessKeyId, + secretAccessKey: awsSecretAccessKey, }); const jsonFeed = await getCurrentJsonFeed(dlcenterArtifacts); @@ -149,13 +155,10 @@ export async function updateJsonFeedCTA( throw new Error('No existing JSON feed found'); } - jsonFeed.cta = config.ctas['*']; - for (const version of jsonFeed.versions) { - version.cta = config.ctas[version.version]; - } + populateJsonFeedCTAs(jsonFeed, config); const patchedJsonFeed = JSON.stringify(jsonFeed, null, 2); - if (config.isDryRun) { + if (isDryRun) { console.warn('Not uploading JSON feed in dry-run mode'); console.warn(`Patched JSON feed: ${patchedJsonFeed}`); return; @@ -164,6 +167,13 @@ export async function updateJsonFeedCTA( await dlcenterArtifacts.uploadAsset(JSON_FEED_ARTIFACT_KEY, patchedJsonFeed); } +function populateJsonFeedCTAs(jsonFeed: JsonFeed, ctas: CTAConfig) { + jsonFeed.cta = ctas['*']; + for (const version of jsonFeed.versions) { + version.cta = ctas[version.version]; + } +} + export function getUpdatedDownloadCenterConfig( downloadedConfig: DownloadCenterConfig, getVersionConfig: () => ReturnType<typeof createVersionConfig> @@ -237,23 +247,6 @@ export function createVersionConfig( }; } -// TODO: this is duplicated in update-notification-manager.ts -interface GreetingCTADetails { - chunks: { - text: string; - style?: string; // TODO: this is actually clr.ts/StyleDefinition - }[]; -} - -export interface UpdateCTAConfig { - ctas: { - [version: string | '*']: GreetingCTADetails; - }; - awsAccessKeyId: string; - awsSecretAccessKey: string; - isDryRun: boolean; -} - export interface JsonFeed { versions: JsonFeedVersionEntry[]; cta?: GreetingCTADetails; diff --git a/packages/build/src/index.ts b/packages/build/src/index.ts index 86791aa466..f818ee6eda 100644 --- a/packages/build/src/index.ts +++ b/packages/build/src/index.ts @@ -7,7 +7,7 @@ import type { ReleaseCommand } from './release'; import { release } from './release'; import type { Config, PackageVariant } from './config'; import { updateJsonFeedCTA } from './download-center'; -import type { UpdateCTAConfig } from './download-center'; +import Ajv from 'ajv'; export { getArtifactUrl, downloadMongoDb }; @@ -30,6 +30,37 @@ const isValidCommand = ( ): cmd is ReleaseCommand | 'trigger-release' | 'update-cta' => (validCommands as string[]).includes(cmd); +const getBuildConfig = (): Config => { + const config: Config = require(path.join( + __dirname, + '..', + '..', + '..', + 'config', + 'build.conf.js' + )); + + const cliBuildVariant = process.argv + .map((arg) => /^--build-variant=(.+)$/.exec(arg)) + .filter(Boolean)[0]; + if (cliBuildVariant) { + config.packageVariant = cliBuildVariant[1] as PackageVariant; + validatePackageVariant(config.packageVariant); + } + + const ajv = new Ajv(); + const validateSchema = ajv.compile(config.ctaConfigSchema); + if (!validateSchema(config.ctaConfig)) { + console.warn('CTA schema validation failed:', validateSchema.errors); + throw new Error('CTA validation failed, see above for details'); + } + + config.isDryRun ||= process.argv.includes('--dry-run'); + config.useAuxiliaryPackagesOnly ||= process.argv.includes('--auxiliary'); + + return config; +}; + if (require.main === module) { Error.stackTraceLimit = 200; @@ -46,40 +77,26 @@ if (require.main === module) { await triggerRelease(process.argv.slice(3)); break; case 'update-cta': - const ctaConfig: UpdateCTAConfig = require(path.join( - __dirname, - '..', - '..', - '..', - 'config', - 'cta.conf.js' - )); + const { + ctaConfig, + downloadCenterAwsKey, + downloadCenterAwsSecret, + isDryRun, + } = getBuildConfig(); - ctaConfig.isDryRun ||= process.argv.includes('--dry-run'); + if (!downloadCenterAwsKey || !downloadCenterAwsSecret) { + throw new Error('Missing AWS credentials for download center'); + } - await updateJsonFeedCTA(ctaConfig); + await updateJsonFeedCTA( + ctaConfig, + downloadCenterAwsKey, + downloadCenterAwsSecret, + !!isDryRun + ); break; default: - const config: Config = require(path.join( - __dirname, - '..', - '..', - '..', - 'config', - 'build.conf.js' - )); - - const cliBuildVariant = process.argv - .map((arg) => /^--build-variant=(.+)$/.exec(arg)) - .filter(Boolean)[0]; - if (cliBuildVariant) { - config.packageVariant = cliBuildVariant[1] as PackageVariant; - validatePackageVariant(config.packageVariant); - } - - config.isDryRun ||= process.argv.includes('--dry-run'); - config.useAuxiliaryPackagesOnly ||= - process.argv.includes('--auxiliary'); + const config = getBuildConfig(); await release(command, config); break; diff --git a/packages/build/src/publish-mongosh.ts b/packages/build/src/publish-mongosh.ts index f1cae0a556..c3a1a03393 100644 --- a/packages/build/src/publish-mongosh.ts +++ b/packages/build/src/publish-mongosh.ts @@ -92,7 +92,8 @@ export async function publishMongosh( config.downloadCenterAwsKey || '', config.downloadCenterAwsSecret || '', config.injectedJsonFeedFile || '', - !!config.isDryRun + !!config.isDryRun, + config.ctaConfig ); await mongoshGithubRepo.promoteRelease(config); diff --git a/packages/build/test/helpers.ts b/packages/build/test/helpers.ts index 27568d0bc0..1048a89714 100644 --- a/packages/build/test/helpers.ts +++ b/packages/build/test/helpers.ts @@ -75,4 +75,6 @@ export const dummyConfig: Config = Object.freeze({ } as PackageInformation), execNodeVersion: process.version, rootDir: path.resolve(__dirname, '..', '..'), + ctaConfig: {}, + ctaConfigSchema: {}, }); From 2e138015ed669d446aaa46713ec2085d4fff69a0 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev <irinchev@me.com> Date: Mon, 10 Feb 2025 13:13:12 +0100 Subject: [PATCH 7/8] CR comments --- .github/workflows/update-cta.yml | 3 +++ packages/build/src/config/config.ts | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-cta.yml b/.github/workflows/update-cta.yml index 859d40de4f..369134d29a 100644 --- a/.github/workflows/update-cta.yml +++ b/.github/workflows/update-cta.yml @@ -18,6 +18,9 @@ on: required: true default: CTA-Production +permissions: + contents: read + jobs: dry-run: name: Update greeting CTA diff --git a/packages/build/src/config/config.ts b/packages/build/src/config/config.ts index 0c2676f0a1..35001e961d 100644 --- a/packages/build/src/config/config.ts +++ b/packages/build/src/config/config.ts @@ -8,11 +8,14 @@ interface ManPageConfig { fileName: string; } -// TODO: this is duplicated in update-notification-manager.ts +// This needs to match the interface in cli-repl/update-notification-manager.ts export interface GreetingCTADetails { chunks: { text: string; - style?: string; // TODO: this is actually clr.ts/StyleDefinition + // This is actually cli-repl/clr.ts/StyleDefinition, but we can't import it here. + // The correct type is already enforced in json schema, so treating it as a generic + // string is fine. + style?: string; }[]; } From 2030107a074380ade9318f76f3f97a23b4a83888 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev <irinchev@me.com> Date: Tue, 11 Feb 2025 13:19:55 +0100 Subject: [PATCH 8/8] fix lint --- packages/build/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/build/src/config/config.ts b/packages/build/src/config/config.ts index 35001e961d..35c514b764 100644 --- a/packages/build/src/config/config.ts +++ b/packages/build/src/config/config.ts @@ -1,4 +1,4 @@ -import { Schema } from 'ajv'; +import type { Schema } from 'ajv'; import type { PackageInformationProvider } from '../packaging/package'; import type { PackageVariant } from './build-variant';