diff --git a/src/constants.ts b/src/constants.ts index 0345b5a0..31150834 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -36,11 +36,15 @@ 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"; + export const SOURCE_JAVA_UPGRADE = "vscode-java-dependency.java-upgrade"; + export const SOURCE_CVE = "vscode-java-dependency.cve"; } /** diff --git a/src/upgrade/display/notificationManager.ts b/src/upgrade/display/notificationManager.ts index 524184b5..d8e6a461 100644 --- a/src/upgrade/display/notificationManager.ts +++ b/src/upgrade/display/notificationManager.ts @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { commands, ExtensionContext, extensions, window } from "vscode"; +import { commands, ExtensionContext, window } from "vscode"; import { UpgradeReason, type IUpgradeIssuesRenderer, type UpgradeIssue } from "../type"; -import { buildCVENotificationMessage, buildFixPrompt, buildNotificationMessage } from "../utility"; +import { buildCVENotificationMessage, buildFixPrompt, buildNotificationMessage, getExtensionState } 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"; const KEY_PREFIX = 'javaupgrade.notificationManager'; @@ -17,6 +17,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; @@ -61,20 +63,36 @@ class NotificationManager implements IUpgradeIssuesRenderer { } this.hasShown = true; - const hasExtension = !!extensions.getExtension(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA); + const extensionState = getExtensionState(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA); const prompt = buildFixPrompt(issue); let notificationMessage = ""; if (hasCVEIssue) { - notificationMessage = buildCVENotificationMessage(cveIssues, hasExtension); + notificationMessage = buildCVENotificationMessage(cveIssues, extensionState); } else { - notificationMessage = buildNotificationMessage(issue, hasExtension); + notificationMessage = buildNotificationMessage(issue, 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; } - 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", + extensionState, }); const buttons = hasCVEIssue @@ -91,9 +109,12 @@ class NotificationManager implements IUpgradeIssuesRenderer { }); switch (selection) { - case fixCVEButtonText: + case fixCVEButtonText: { + commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt, Upgrade.SOURCE_CVE); + break; + } case upgradeButtonText: { - commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt); + commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt, Upgrade.SOURCE_JAVA_UPGRADE); break; } case BUTTON_TEXT_NOT_NOW: { diff --git a/src/upgrade/upgradeManager.ts b/src/upgrade/upgradeManager.ts index 1f9f1d3a..90a60b2a 100644 --- a/src/upgrade/upgradeManager.ts +++ b/src/upgrade/upgradeManager.ts @@ -5,7 +5,7 @@ import { commands, type ExtensionContext, workspace, type WorkspaceFolder } from import { Jdtls } from "../java/jdtls"; import { languageServerApiManager } from "../languageServerApi/languageServerApiManager"; -import { ExtensionName } from "../constants"; +import { ExtensionName, Upgrade } from "../constants"; import { instrumentOperation, instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper"; import { Commands } from "../commands"; import notificationManager from "./display/notificationManager"; @@ -25,10 +25,18 @@ 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); + context.subscriptions.push(instrumentOperationAsVsCodeCommand( + Commands.JAVA_UPGRADE_WITH_COPILOT, async (promptText?: string, source?: string) => { + const canProceed = await checkOrInstallAppModExtensionForUpgrade( + ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA); + if (!canProceed) { + return; + } const promptToUse = promptText ?? DEFAULT_UPGRADE_PROMPT; - await commands.executeCommand(Commands.GOTO_AGENT_MODE, { prompt: promptToUse, useCustomAgent: true }); + const upgradeSource = source ?? Upgrade.SOURCE_JAVA_UPGRADE; + await commands.executeCommand(Commands.GOTO_AGENT_MODE, { + prompt: promptToUse, useCustomAgent: true, source: upgradeSource, + }); })); // Show modernization view diff --git a/src/upgrade/utility.ts b/src/upgrade/utility.ts index 40fb2de2..97461b75 100644 --- a/src/upgrade/utility.ts +++ b/src/upgrade/utility.ts @@ -22,7 +22,33 @@ function findEolDate(currentVersion: string, eolDate: Record): s return null; } -export function buildNotificationMessage(issue: UpgradeIssue, hasExtension: boolean): string { +export type ExtensionState = "up-to-date" | "outdated" | "not-installed"; + +export function getExtensionState(extensionId: string): ExtensionState { + const ext = extensions.getExtension(extensionId); + if (!ext) { + return "not-installed"; + } + const version = ext.packageJSON?.version; + if (version && semver.gte(version, Upgrade.MIN_APPMOD_VERSION)) { + return "up-to-date"; + } + // Treat missing version as outdated (conservative) + return "outdated"; +} + +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, @@ -31,7 +57,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?`; @@ -51,7 +77,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."; @@ -81,7 +107,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?`; @@ -154,11 +180,57 @@ export async function checkOrPopupToInstallAppModExtensionForModernization( } export async function checkOrInstallAppModExtensionForUpgrade( - extensionIdToCheck: string): Promise { - if (extensions.getExtension(extensionIdToCheck)) { - return; - } + extensionIdToCheck: string): Promise { + return instrumentOperation("java.dependency.upgradeFlow", async (operationId: string) => { + const state = getExtensionState(extensionIdToCheck); + sendInfo(operationId, { + operationName: "java.dependency.upgradeFlow.start", + extensionState: state, + }); + + if (state === "up-to-date") { + sendInfo(operationId, { + operationName: "java.dependency.upgradeFlow.result", + upgradeFlowResult: "proceeded", + }); + return true; + } - await commands.executeCommand("workbench.extensions.installExtension", ExtensionName.APP_MODERNIZATION_FOR_JAVA); - await checkOrPromptToEnableAppModExtension("upgrade"); + await commands.executeCommand("workbench.extensions.installExtension", ExtensionName.APP_MODERNIZATION_FOR_JAVA); + + if (state === "outdated") { + // Extension was updated (not freshly installed) — reload required + const reload = await window.showInformationMessage( + `${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension has been updated. Reload VS Code to start the upgrade experience.`, + "Reload Now" + ); + if (reload === "Reload Now") { + sendInfo(operationId, { + operationName: "java.dependency.upgradeFlow.result", + upgradeFlowResult: "reload-accepted", + }); + await commands.executeCommand("workbench.action.reloadWindow"); + } else { + sendInfo(operationId, { + operationName: "java.dependency.upgradeFlow.result", + upgradeFlowResult: "reload-dismissed", + }); + } + return false; + } + + await checkOrPromptToEnableAppModExtension("upgrade"); + + // Wait briefly for the newly installed extension to activate + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Re-check if the newly installed extension is active and meets version requirement + const newState = getExtensionState(extensionIdToCheck); + const canProceed = newState === "up-to-date"; + sendInfo(operationId, { + operationName: "java.dependency.upgradeFlow.result", + upgradeFlowResult: canProceed ? "proceeded" : "activation-timeout", + }); + return canProceed; + })(); } \ No newline at end of file