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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ export namespace ExtensionName {
export const APP_MODERNIZATION_FOR_JAVA = "vscjava.migrate-java-to-azure";
// Java upgrade extension is merged into app modernization extension
export const APP_MODERNIZATION_UPGRADE_FOR_JAVA = APP_MODERNIZATION_FOR_JAVA;
export const APP_MODERNIZATION_EXTENSION_NAME = "GitHub Copilot app modernization";
export const APP_MODERNIZATION_EXTENSION_NAME = "GitHub Copilot modernization";
}

export namespace Upgrade {
export const PACKAGE_ID_FOR_JAVA_RUNTIME = "java:*";
/** Minimum version of the appmod extension that supports gotoAgentMode command */
export const MIN_APPMOD_VERSION = "1.15.0";
}

/**
Expand Down
104 changes: 78 additions & 26 deletions src/upgrade/display/notificationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// Licensed under the MIT license.

import { commands, ExtensionContext, extensions, window } from "vscode";
import * as semver from "semver";
import { UpgradeReason, type IUpgradeIssuesRenderer, type UpgradeIssue } from "../type";
import { buildCVENotificationMessage, buildFixPrompt, buildNotificationMessage } from "../utility";
import { buildCVENotificationMessage, buildFixPrompt, buildNotificationMessage, type ExtensionState } from "../utility";
import { Commands } from "../../commands";
import { Settings } from "../../settings";
import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
import { ExtensionName } from "../../constants";
import { ExtensionName, Upgrade } from "../../constants";
import { CveUpgradeIssue } from "../cve";
import { UpgradeTelemetry } from "../telemetryConstants";

const KEY_PREFIX = 'javaupgrade.notificationManager';
const NEXT_SHOW_TS_KEY = `${KEY_PREFIX}.nextShowTs`;
Expand All @@ -17,6 +19,8 @@ const BUTTON_TEXT_UPGRADE = "Upgrade Now";
const BUTTON_TEXT_FIX_CVE = "Fix Now";
const BUTTON_TEXT_INSTALL_AND_UPGRADE = "Install Extension and Upgrade";
const BUTTON_TEXT_INSTALL_AND_FIX_CVE = "Install Extension and Fix";
const BUTTON_TEXT_UPDATE_AND_UPGRADE = "Update Extension and Upgrade";
const BUTTON_TEXT_UPDATE_AND_FIX_CVE = "Update Extension and Fix";
const BUTTON_TEXT_NOT_NOW = "Not Now";

const SECONDS_IN_A_DAY = 24 * 60 * 60;
Expand All @@ -26,6 +30,61 @@ function getNowTs() {
return Number(new Date()) / 1000;
}

export type { ExtensionState } from "../utility";

export interface NotificationContent {
message: string;
upgradeButtonText: string;
fixCVEButtonText: string;
}

export function getExtensionState(extensionVersion: string | undefined): ExtensionState {
if (!extensionVersion) {
return "not-installed";
}
if (semver.gte(extensionVersion, Upgrade.MIN_APPMOD_VERSION)) {
return "up-to-date";
}
return "outdated";
}

export function buildNotificationContent(
issues: UpgradeIssue[],
extensionState: ExtensionState,
): NotificationContent {
const cveIssues = issues.filter(
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
);
const nonCVEIssues = issues.filter(
(i) => i.reason !== UpgradeReason.CVE
);
const hasCVEIssue = cveIssues.length > 0;

const message = hasCVEIssue
? buildCVENotificationMessage(cveIssues, extensionState)
: buildNotificationMessage(nonCVEIssues[0], extensionState);

let upgradeButtonText: string;
let fixCVEButtonText: string;

switch (extensionState) {
case "up-to-date":
upgradeButtonText = BUTTON_TEXT_UPGRADE;
fixCVEButtonText = BUTTON_TEXT_FIX_CVE;
break;
case "outdated":
upgradeButtonText = BUTTON_TEXT_UPDATE_AND_UPGRADE;
fixCVEButtonText = BUTTON_TEXT_UPDATE_AND_FIX_CVE;
break;
case "not-installed":
upgradeButtonText = BUTTON_TEXT_INSTALL_AND_UPGRADE;
fixCVEButtonText = BUTTON_TEXT_INSTALL_AND_FIX_CVE;
break;
}

return { message, upgradeButtonText, fixCVEButtonText };
}

class NotificationManager implements IUpgradeIssuesRenderer {
private hasShown = false;
private context?: ExtensionContext;
Expand All @@ -42,16 +101,6 @@ class NotificationManager implements IUpgradeIssuesRenderer {
return;
}

// Filter to only CVE issues and cast to CveUpgradeIssue[]
const cveIssues = issues.filter(
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
);
const nonCVEIssues = issues.filter(
(i) => i.reason !== UpgradeReason.CVE
);
const hasCVEIssue = cveIssues.length > 0;
const issue = hasCVEIssue ? cveIssues[0] : nonCVEIssues[0];

if (!this.shouldShow()) {
return;
}
Expand All @@ -61,39 +110,42 @@ class NotificationManager implements IUpgradeIssuesRenderer {
}
this.hasShown = true;

const hasExtension = !!extensions.getExtension(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
const prompt = buildFixPrompt(issue);
const ext = extensions.getExtension(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
const extensionState = getExtensionState(ext?.packageJSON?.version);
const { message, upgradeButtonText, fixCVEButtonText } = buildNotificationContent(issues, extensionState);

let notificationMessage = "";
const hasCVEIssue = issues.some(i => i.reason === UpgradeReason.CVE);
const issueType = hasCVEIssue ? "cve" : "upgrade";
const issue = hasCVEIssue
? issues.find((i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE)!
: issues.find(i => i.reason !== UpgradeReason.CVE)!;
const prompt = buildFixPrompt(issue);

if (hasCVEIssue) {
notificationMessage = buildCVENotificationMessage(cveIssues, hasExtension);
} else {
notificationMessage = buildNotificationMessage(issue, hasExtension);
}
const upgradeButtonText = hasExtension ? BUTTON_TEXT_UPGRADE : BUTTON_TEXT_INSTALL_AND_UPGRADE;
const fixCVEButtonText = hasExtension ? BUTTON_TEXT_FIX_CVE : BUTTON_TEXT_INSTALL_AND_FIX_CVE;
sendInfo(operationId, {
operationName: "java.dependency.upgradeNotification.show",
operationName: UpgradeTelemetry.NOTIFICATION_SHOW,
extensionState,
issueType,
});

const buttons = hasCVEIssue
? [fixCVEButtonText, BUTTON_TEXT_NOT_NOW]
: [upgradeButtonText, BUTTON_TEXT_NOT_NOW];

const selection = await window.showInformationMessage(
notificationMessage,
message,
...buttons
);
sendInfo(operationId, {
operationName: "java.dependency.upgradeNotification.runUpgrade",
operationName: UpgradeTelemetry.NOTIFICATION_CLICK,
extensionState,
issueType,
choice: selection ?? "",
});

switch (selection) {
case fixCVEButtonText:
case upgradeButtonText: {
commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt);
commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt, issueType, extensionState, operationId);
break;
}
case BUTTON_TEXT_NOT_NOW: {
Expand Down
13 changes: 13 additions & 0 deletions src/upgrade/telemetryConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Telemetry operation names for the upgrade flow.
*/
export const UpgradeTelemetry = {
NOTIFICATION_SHOW: "java.dependency.upgradeNotification.show",
NOTIFICATION_CLICK: "java.dependency.upgradeNotification.runUpgrade",
EXECUTE_START: "java.dependency.upgrade.execute.start",
EXECUTE_END: "java.dependency.upgrade.execute.end",
EXTENSION_INSTALL_START: "java.dependency.upgrade.extensionInstall.start",
EXTENSION_INSTALL_END: "java.dependency.upgrade.extensionInstall.end",
RELOAD_PROMPT_SHOW: "java.dependency.upgrade.reloadPrompt.show",
RELOAD_PROMPT_CLICK: "java.dependency.upgrade.reloadPrompt.click",
} as const;
30 changes: 25 additions & 5 deletions src/upgrade/upgradeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import notificationManager from "./display/notificationManager";
import { Settings } from "../settings";
import assessmentManager, { getDirectDependencies } from "./assessmentManager";
import { checkOrInstallAppModExtensionForUpgrade, checkOrPopupToInstallAppModExtensionForModernization } from "./utility";
import { UpgradeTelemetry } from "./telemetryConstants";

const DEFAULT_UPGRADE_PROMPT = "Upgrade Java project dependency to latest version.";
const DEFAULT_UPGRADE_PROMPT = "Upgrade Java runtime and frameworks to the latest stable version.";


function shouldRunCheckup() {
Expand All @@ -25,10 +26,29 @@ class UpgradeManager {
notificationManager.initialize(context);

// Upgrade project
context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, async (promptText?: string) => {
await checkOrInstallAppModExtensionForUpgrade(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
const promptToUse = promptText ?? DEFAULT_UPGRADE_PROMPT;
await commands.executeCommand(Commands.GOTO_AGENT_MODE, { prompt: promptToUse, useCustomAgent: true });
context.subscriptions.push(commands.registerCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, (promptText?: string, issueType?: string, extensionState?: string, notificationOperationId?: string) => {
return instrumentOperation(Commands.JAVA_UPGRADE_WITH_COPILOT, async (operationId: string) => {
const dimensions = {
issueType: issueType ?? "unknown",
extensionState: extensionState ?? "unknown",
notificationOperationId: notificationOperationId ?? "",
};
sendInfo(operationId, { operationName: UpgradeTelemetry.EXECUTE_START, ...dimensions });
try {
await checkOrInstallAppModExtensionForUpgrade(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
const promptToUse = promptText ?? DEFAULT_UPGRADE_PROMPT;
await commands.executeCommand(Commands.GOTO_AGENT_MODE, { prompt: promptToUse, useCustomAgent: true });
sendInfo(operationId, { operationName: UpgradeTelemetry.EXECUTE_END, ...dimensions, result: "success" });
} catch (e) {
sendInfo(operationId, {
operationName: UpgradeTelemetry.EXECUTE_END,
...dimensions,
result: "failure",
error: e instanceof Error ? e.message : String(e),
});
throw e;
}
})();
}));

// Show modernization view
Expand Down
61 changes: 54 additions & 7 deletions src/upgrade/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { UpgradeReason, type UpgradeIssue } from "./type";
import { ExtensionName, Upgrade } from "../constants";
import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
import { CveUpgradeIssue } from "./cve";
import { UpgradeTelemetry } from "./telemetryConstants";


function findEolDate(currentVersion: string, eolDate: Record<string, string>): string | null {
Expand All @@ -22,7 +23,20 @@ function findEolDate(currentVersion: string, eolDate: Record<string, string>): s
return null;
}

export function buildNotificationMessage(issue: UpgradeIssue, hasExtension: boolean): string {
export type ExtensionState = "up-to-date" | "outdated" | "not-installed";

function getActionWord(extensionState: ExtensionState, verb: string): string {
switch (extensionState) {
case "up-to-date":
return verb;
case "outdated":
return `update ${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension and ${verb}`;
case "not-installed":
return `install ${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension and ${verb}`;
}
}

export function buildNotificationMessage(issue: UpgradeIssue, extensionState: ExtensionState): string {
const {
packageId,
currentVersion,
Expand All @@ -31,7 +45,7 @@ export function buildNotificationMessage(issue: UpgradeIssue, hasExtension: bool
packageDisplayName
} = issue;

const upgradeWord = hasExtension ? "upgrade" : `install ${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension and upgrade`;
const upgradeWord = getActionWord(extensionState, "upgrade");

if (packageId === Upgrade.PACKAGE_ID_FOR_JAVA_RUNTIME) {
return `This project is using an older Java runtime (${currentVersion}). Would you like to ${upgradeWord} it to the latest LTS version?`;
Expand All @@ -51,7 +65,7 @@ export function buildNotificationMessage(issue: UpgradeIssue, hasExtension: bool
}
}

export function buildCVENotificationMessage(issues: CveUpgradeIssue[], hasExtension: boolean): string {
export function buildCVENotificationMessage(issues: CveUpgradeIssue[], extensionState: ExtensionState): string {

if (issues.length === 0) {
return "No CVE issues found.";
Expand Down Expand Up @@ -81,7 +95,7 @@ export function buildCVENotificationMessage(issues: CveUpgradeIssue[], hasExtens
CVESeverityDistribution: severityText,
});

const fixWord = hasExtension ? "fix" : `install ${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension and fix`;
const fixWord = getActionWord(extensionState, "fix");

if (issues.length === 1) {
return `${severityText} CVE vulnerability is detected in this project. Would you like to ${fixWord} it now?`;
Expand All @@ -102,7 +116,7 @@ export function buildFixPrompt(issue: UpgradeIssue): string {
return `upgrade ${packageDisplayName} to ${suggestedVersionName}`;
}
case UpgradeReason.CVE: {
return `fix all critical and high-severity CVE vulnerabilities in this project by invoking #appmod-validate-cves-for-java`;
return `fix all CVE vulnerabilities in this project`;
}
}
}
Expand Down Expand Up @@ -155,10 +169,43 @@ export async function checkOrPopupToInstallAppModExtensionForModernization(

export async function checkOrInstallAppModExtensionForUpgrade(
extensionIdToCheck: string): Promise<void> {
if (extensions.getExtension(extensionIdToCheck)) {
const ext = extensions.getExtension(extensionIdToCheck);

if (ext) {
const installedVersion = ext.packageJSON?.version;
if (installedVersion && semver.gte(installedVersion, Upgrade.MIN_APPMOD_VERSION)) {
return;
}
}

const action = ext ? "update" : "install";
sendInfo("", { operationName: UpgradeTelemetry.EXTENSION_INSTALL_START, action });
try {
await commands.executeCommand("workbench.extensions.installExtension", ExtensionName.APP_MODERNIZATION_FOR_JAVA);
sendInfo("", { operationName: UpgradeTelemetry.EXTENSION_INSTALL_END, action, result: "success" });
} catch (e) {
sendInfo("", {
operationName: UpgradeTelemetry.EXTENSION_INSTALL_END,
action,
result: "failure",
error: e instanceof Error ? e.message : String(e),
});
throw e;
}

if (action === "update") {
// Reload is required for the updated extension to take effect
sendInfo("", { operationName: UpgradeTelemetry.RELOAD_PROMPT_SHOW });
const reload = await window.showInformationMessage(
"GitHub Copilot modernization extension has been updated. Reload VS Code to start the modernize experience.",
"Reload Now"
);
sendInfo("", { operationName: UpgradeTelemetry.RELOAD_PROMPT_CLICK, choice: reload ?? "dismissed" });
if (reload === "Reload Now") {
await commands.executeCommand("workbench.action.reloadWindow");
}
return;
}

await commands.executeCommand("workbench.extensions.installExtension", ExtensionName.APP_MODERNIZATION_FOR_JAVA);
await checkOrPromptToEnableAppModExtension("upgrade");
}
Loading
Loading