diff --git a/plugins/amazonq/codetransform/jetbrains-community/resources/META-INF/codetransform-ext-java.xml b/plugins/amazonq/codetransform/jetbrains-community/resources/META-INF/codetransform-ext-java.xml index 614c5e18b10..9a4bb0e5d25 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/resources/META-INF/codetransform-ext-java.xml +++ b/plugins/amazonq/codetransform/jetbrains-community/resources/META-INF/codetransform-ext-java.xml @@ -18,7 +18,7 @@ + icon="AllIcons.Actions.Properties"/> diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt index 93d3da48e7e..ab4af2a8d4c 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt @@ -24,7 +24,6 @@ import software.aws.toolkits.core.utils.exists import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info import software.aws.toolkits.jetbrains.core.coroutines.EDT -import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.NoTokenInitializedException import software.aws.toolkits.jetbrains.services.amazonq.CODE_TRANSFORM_TROUBLESHOOT_DOC_DOWNLOAD_ERROR_OVERVIEW @@ -50,13 +49,13 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.state.CodeModerni import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getPathToHilArtifactDir import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isValidCodeTransformConnection import software.aws.toolkits.jetbrains.services.codemodernizer.utils.openTroubleshootingGuideNotificationAction +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.zipToPath import software.aws.toolkits.jetbrains.utils.notifyInfo import software.aws.toolkits.jetbrains.utils.notifyStickyInfo import software.aws.toolkits.jetbrains.utils.notifyStickyWarn import software.aws.toolkits.resources.message import software.aws.toolkits.telemetry.CodeTransformArtifactType import java.io.File -import java.nio.file.Files import java.nio.file.Path import java.time.Instant import java.util.concurrent.atomic.AtomicBoolean @@ -112,32 +111,12 @@ class ArtifactHandler( } } - suspend fun unzipToPath(byteArrayList: List, outputDirPath: Path? = null): Pair { - val zipFilePath = withContext(getCoroutineBgContext()) { - if (outputDirPath == null) { - Files.createTempFile(null, ".zip") - } else { - Files.createTempFile(outputDirPath, null, ".zip") - } - } - var totalDownloadBytes = 0 - withContext(getCoroutineBgContext()) { - Files.newOutputStream(zipFilePath).use { - for (bytes in byteArrayList) { - it.write(bytes) - totalDownloadBytes += bytes.size - } - } - } - return zipFilePath to totalDownloadBytes - } - suspend fun downloadHilArtifact(jobId: JobId, artifactId: String, tmpDir: File): CodeTransformHilDownloadArtifact? { val downloadResultsResponse = clientAdaptor.downloadExportResultArchive(jobId, artifactId) return try { val tmpPath = tmpDir.toPath() - val (downloadZipFilePath, _) = unzipToPath(downloadResultsResponse, tmpPath) + val (downloadZipFilePath, _) = zipToPath(downloadResultsResponse, tmpPath) LOG.info { "Successfully converted the hil artifact download to a zip at ${downloadZipFilePath.toAbsolutePath()}." } CodeTransformHilDownloadArtifact.create(downloadZipFilePath, getPathToHilArtifactDir(tmpPath)) } catch (e: Exception) { @@ -211,7 +190,7 @@ class ArtifactHandler( val totalDownloadBytes: Int val zipPath: String try { - val result = unzipToPath(downloadResultsResponse) + val result = zipToPath(downloadResultsResponse) path = result.first totalDownloadBytes = result.second zipPath = path.toAbsolutePath().toString() diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt index 9b2db462a41..77b95e0e255 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt @@ -909,7 +909,7 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo // Add delay between upload complete and trying to resume delay(500) - codeTransformationSession?.resumeTransformFromHil() + codeTransformationSession?.resumeTransformation() } else { throw CodeModernizerException("Cannot create dependency zip for HIL") } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt index 921a9ac0f2c..b1ff57d6c8d 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt @@ -20,6 +20,7 @@ import software.amazon.awssdk.services.codewhispererruntime.model.Transformation import software.amazon.awssdk.services.codewhispererruntime.model.TransformationProgressUpdateStatus import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStatus import software.amazon.awssdk.services.codewhispererruntime.model.TransformationUserActionStatus +import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext import software.amazon.awssdk.services.codewhispererstreaming.model.TransformationDownloadArtifactType import software.amazon.awssdk.services.ssooidc.model.SsoOidcException import software.aws.toolkits.core.utils.Waiters.waitUntil @@ -174,6 +175,7 @@ class CodeModernizerSession( } // for language upgrades, copyResult should always be Successful here, failure cases already handled val result = sessionContext.createZipWithModuleFiles(copyResult) + sessionContext.originalUploadZipPath = result.payload.toPath() if (result is ZipCreationResult.Missing1P) { telemetryErrorMessage = "Missing 1p dependencies" @@ -283,9 +285,7 @@ class CodeModernizerSession( return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.OTHER(e.localizedMessage)) } finally { telemetry.uploadProject(payloadSize, startTime, true, telemetryErrorMessage) - if (payload != null) { - deleteUploadArtifact(payload) - } + // do not delete upload ZIP; re-used for client-side build } // Send upload completion message to chat (only if successful) @@ -309,12 +309,6 @@ class CodeModernizerSession( } } - internal fun deleteUploadArtifact(payload: File) { - if (!payload.delete()) { - LOG.warn { "Unable to delete upload artifact." } - } - } - private fun startJob(uploadId: String): StartTransformationResponse { val sourceLanguage = sessionContext.sourceJavaVersion.name.toTransformationLanguage() val targetLanguage = sessionContext.targetJavaVersion.name.toTransformationLanguage() @@ -341,9 +335,10 @@ class CodeModernizerSession( */ fun resumeJob(startTime: Instant, jobId: JobId) = state.putJobHistory(sessionContext, TransformationStatus.STARTED, jobId.id, startTime) - fun resumeTransformFromHil() { + fun resumeTransformation() { val clientAdaptor = GumbyClient.getInstance(sessionContext.project) clientAdaptor.resumeCodeTransformation(state.currentJobId as JobId, TransformationUserActionStatus.COMPLETED) + getLogger().info { "Successfully resumed transformation with status of COMPLETED" } } fun rejectHilAndContinue(): ResumeTransformationResponse { @@ -390,7 +385,7 @@ class CodeModernizerSession( /** * Adapted from [CodeWhispererCodeScanSession] */ - suspend fun uploadPayload(payload: File): String { + suspend fun uploadPayload(payload: File, uploadContext: UploadContext? = null): String { val sha256checksum: String = Base64.getEncoder().encodeToString( withContext(getCoroutineBgContext()) { DigestUtils.sha256(FileInputStream(payload)) @@ -400,7 +395,7 @@ class CodeModernizerSession( throw AlreadyDisposedException("Disposed when about to create upload URL") } val clientAdaptor = GumbyClient.getInstance(sessionContext.project) - val createUploadUrlResponse = clientAdaptor.createGumbyUploadUrl(sha256checksum) + val createUploadUrlResponse = clientAdaptor.createGumbyUploadUrl(sha256checksum, uploadContext) LOG.info { "Uploading project artifact at ${payload.path} with checksum $sha256checksum using uploadId: ${ @@ -428,9 +423,9 @@ class CodeModernizerSession( createUploadUrlResponse.kmsKeyArn().orEmpty(), ) { shouldStop.get() } } - LOG.info { "Upload to S3 succeeded" } + LOG.info { "Upload of ${payload.path} to S3 succeeded with upload context of $uploadContext" } if (!shouldStop.get()) { - LOG.info { "Uploaded artifact. Latency: ${calculateTotalLatency(uploadStartTime, Instant.now())}ms" } + LOG.info { "Uploaded artifact. Latency: ${calculateTotalLatency(uploadStartTime, Instant.now())} ms" } } return createUploadUrlResponse.uploadId() } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt index b102d348d11..edbf0afa7be 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt @@ -41,9 +41,11 @@ private enum class CodeTransformMessageTypes(val type: String) { ChatPrompt("chat-prompt"), // for getting the transformation objective CodeTransformStart("codetransform-start"), CodeTransformSelectSQLMetadata("codetransform-select-sql-metadata"), + CodeTransformConfirmCustomDependencyVersions("codetransform-input-confirm-custom-dependency-versions"), CodeTransformSelectSQLModuleSchema("codetransform-select-sql-module-schema"), CodeTransformStop("codetransform-stop"), CodeTransformCancel("codetransform-cancel"), + CodeTransformContinue("codetransform-continue"), CodeTransformConfirmSkipTests("codetransform-confirm-skip-tests"), CodeTransformConfirmOneOrMultipleDiffs("codetransform-confirm-one-or-multiple-diffs"), CodeTransformNew("codetransform-new"), @@ -74,9 +76,12 @@ class CodeTransformChatApp : AmazonQApp { CodeTransformMessageTypes.Transform.type to IncomingCodeTransformMessage.Transform::class, CodeTransformMessageTypes.CodeTransformStart.type to IncomingCodeTransformMessage.CodeTransformStart::class, CodeTransformMessageTypes.CodeTransformSelectSQLMetadata.type to IncomingCodeTransformMessage.CodeTransformSelectSQLMetadata::class, + CodeTransformMessageTypes.CodeTransformConfirmCustomDependencyVersions.type to + IncomingCodeTransformMessage.CodeTransformConfirmCustomDependencyVersions::class, CodeTransformMessageTypes.CodeTransformSelectSQLModuleSchema.type to IncomingCodeTransformMessage.CodeTransformSelectSQLModuleSchema::class, CodeTransformMessageTypes.CodeTransformStop.type to IncomingCodeTransformMessage.CodeTransformStop::class, CodeTransformMessageTypes.CodeTransformCancel.type to IncomingCodeTransformMessage.CodeTransformCancel::class, + CodeTransformMessageTypes.CodeTransformContinue.type to IncomingCodeTransformMessage.CodeTransformContinue::class, CodeTransformMessageTypes.ChatPrompt.type to IncomingCodeTransformMessage.ChatPrompt::class, CodeTransformMessageTypes.CodeTransformConfirmSkipTests.type to IncomingCodeTransformMessage.CodeTransformConfirmSkipTests::class, CodeTransformMessageTypes.CodeTransformConfirmOneOrMultipleDiffs.type to IncomingCodeTransformMessage.CodeTransformConfirmOneOrMultipleDiffs::class, @@ -182,12 +187,14 @@ class CodeTransformChatApp : AmazonQApp { is IncomingCodeTransformMessage.CodeTransformSelectSQLMetadata -> inboundAppMessagesHandler.processCodeTransformSelectSQLMetadataAction(message) is IncomingCodeTransformMessage.CodeTransformSelectSQLModuleSchema -> inboundAppMessagesHandler.processCodeTransformSelectSQLModuleSchemaAction(message) - + is IncomingCodeTransformMessage.CodeTransformConfirmCustomDependencyVersions -> + inboundAppMessagesHandler.processCodeTransformCustomDependencyVersions(message) is IncomingCodeTransformMessage.CodeTransformCancel -> inboundAppMessagesHandler.processCodeTransformCancelAction(message) is IncomingCodeTransformMessage.CodeTransformStop -> inboundAppMessagesHandler.processCodeTransformStopAction(message.tabId) is IncomingCodeTransformMessage.ChatPrompt -> inboundAppMessagesHandler.processChatPromptMessage(message) is IncomingCodeTransformMessage.CodeTransformConfirmSkipTests -> inboundAppMessagesHandler.processCodeTransformConfirmSkipTests(message) is IncomingCodeTransformMessage.CodeTransformConfirmOneOrMultipleDiffs -> inboundAppMessagesHandler.processCodeTransformOneOrMultipleDiffs(message) + is IncomingCodeTransformMessage.CodeTransformContinue -> inboundAppMessagesHandler.processCodeTransformContinueAction(message) is IncomingCodeTransformMessage.CodeTransformNew -> inboundAppMessagesHandler.processCodeTransformNewAction(message) is IncomingCodeTransformMessage.CodeTransformOpenTransformHub -> inboundAppMessagesHandler.processCodeTransformOpenTransformHub(message) is IncomingCodeTransformMessage.CodeTransformOpenMvnBuild -> inboundAppMessagesHandler.processCodeTransformOpenMvnBuild(message) diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/InboundAppMessagesHandler.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/InboundAppMessagesHandler.kt index 1312328243e..434516178bd 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/InboundAppMessagesHandler.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/InboundAppMessagesHandler.kt @@ -11,12 +11,16 @@ interface InboundAppMessagesHandler { suspend fun processCodeTransformCancelAction(message: IncomingCodeTransformMessage.CodeTransformCancel) + suspend fun processCodeTransformContinueAction(message: IncomingCodeTransformMessage.CodeTransformContinue) + suspend fun processCodeTransformStartAction(message: IncomingCodeTransformMessage.CodeTransformStart) suspend fun processCodeTransformSelectSQLMetadataAction(message: IncomingCodeTransformMessage.CodeTransformSelectSQLMetadata) suspend fun processCodeTransformSelectSQLModuleSchemaAction(message: IncomingCodeTransformMessage.CodeTransformSelectSQLModuleSchema) + suspend fun processCodeTransformCustomDependencyVersions(message: IncomingCodeTransformMessage.CodeTransformConfirmCustomDependencyVersions) + suspend fun processCodeTransformStopAction(tabId: String) suspend fun processChatPromptMessage(message: IncomingCodeTransformMessage.ChatPrompt) diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt index 6134e69ca22..f94e25d166f 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt @@ -61,12 +61,13 @@ class GumbyClient(private val project: Project) { private val amazonQStreamingClient get() = AmazonQStreamingClient.getInstance(project) - fun createGumbyUploadUrl(sha256Checksum: String): CreateUploadUrlResponse { + fun createGumbyUploadUrl(sha256Checksum: String, context: UploadContext? = null): CreateUploadUrlResponse { val request = CreateUploadUrlRequest.builder() .contentChecksumType(ContentChecksumType.SHA_256) .contentChecksum(sha256Checksum) .uploadIntent(UploadIntent.TRANSFORMATION) .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn) + .uploadContext(context) .build() return callApi({ bearerClient().createUploadUrl(request) }, apiName = "CreateUploadUrl") } @@ -164,12 +165,12 @@ class GumbyClient(private val project: Project) { suspend fun downloadExportResultArchive( jobId: JobId, - hilDownloadArtifactId: String? = null, + downloadArtifactId: String? = null, downloadArtifactType: TransformationDownloadArtifactType? = TransformationDownloadArtifactType.CLIENT_INSTRUCTIONS, ): MutableList = amazonQStreamingClient.exportResultArchive( jobId.id, ExportIntent.TRANSFORMATION, - if (hilDownloadArtifactId == null) { + if (downloadArtifactId == null) { null } else { ExportContext @@ -177,14 +178,14 @@ class GumbyClient(private val project: Project) { .transformationExportContext( TransformationExportContext .builder() - .downloadArtifactId(hilDownloadArtifactId) + .downloadArtifactId(downloadArtifactId) .downloadArtifactType(downloadArtifactType.toString()) .build() ) .build() }, { e -> - LOG.error(e) { "ExportResultArchive failed: ${e.message}" } + LOG.error(e) { "ExportResultArchive on job ${jobId.id} and artifact $downloadArtifactId failed: ${e.message}" } }, { startTime -> LOG.info { "ExportResultArchive latency: ${calculateTotalLatency(startTime, Instant.now())}" } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeTransformChatItems.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeTransformChatItems.kt index c39d530e6b2..dc983f6e6d7 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeTransformChatItems.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeTransformChatItems.kt @@ -44,6 +44,14 @@ private val cancelUserSelectionButton = Button( id = CodeTransformButtonId.CancelTransformation.id, ) +// used to continue transformation without providing custom YAML file +private val continueTransformationButton = Button( + keepCardAfterClick = false, + waitMandatoryFormItems = false, + text = "Continue without this", + id = CodeTransformButtonId.ContinueTransformation.id, +) + private val confirmUserSelectionLanguageUpgradeButton = Button( keepCardAfterClick = false, waitMandatoryFormItems = true, @@ -79,6 +87,13 @@ private val confirmOneOrMultipleDiffsSelectionButton = Button( id = CodeTransformButtonId.ConfirmOneOrMultipleDiffs.id, ) +private val confirmCustomDependencyVersionsButton = Button( + keepCardAfterClick = true, + waitMandatoryFormItems = true, + text = "Select file", + id = CodeTransformButtonId.ConfirmCustomDependencyVersions.id, +) + private val openMvnBuildButton = Button( id = CodeTransformButtonId.OpenMvnBuild.id, text = message("codemodernizer.chat.message.button.view_build"), @@ -267,8 +282,8 @@ fun buildChooseTransformationObjectiveChatContent() = CodeTransformChatMessageCo type = CodeTransformChatMessageType.FinalizedAnswer, ) -fun buildObjectiveChosenChatContent(objective: String) = CodeTransformChatMessageContent( - message = objective, +fun buildUserReplyChatContent(reply: String) = CodeTransformChatMessageContent( + message = reply, type = CodeTransformChatMessageType.Prompt, ) @@ -375,6 +390,31 @@ fun buildUserInputSQLConversionMetadataChatContent() = CodeTransformChatMessageC type = CodeTransformChatMessageType.FinalizedAnswer, ) +fun buildUserInputCustomDependencyVersionsChatContent() = CodeTransformChatMessageContent( + message = "Optionally, provide a .YAML file which specifies custom dependency versions you want Q to upgrade to.", + buttons = listOf( + confirmCustomDependencyVersionsButton, + continueTransformationButton, + ), + type = CodeTransformChatMessageType.PendingAnswer, +) + +fun buildPromptTargetJDKNameChatContent(version: String) = CodeTransformChatMessageContent( + message = message("codemodernizer.chat.message.enter_jdk_name", version), + type = CodeTransformChatMessageType.FinalizedAnswer, +) + +fun buildInvalidTargetJdkNameChatContent(jdkName: String) = CodeTransformChatMessageContent( + message = message("codemodernizer.chat.message.enter_jdk_name_error", jdkName), + type = CodeTransformChatMessageType.FinalizedAnswer, + followUps = listOf(startNewTransformFollowUp) +) + +fun buildCustomDependencyVersionsFileValidChatContent() = CodeTransformChatMessageContent( + message = "I received your .yaml file and will upload it to Q.", + type = CodeTransformChatMessageType.FinalizedAnswer, +) + fun buildModuleSchemaFormChatContent(project: Project, javaModules: List, schemaOptions: Set) = CodeTransformChatMessageContent( type = CodeTransformChatMessageType.FinalizedAnswer, buttons = listOf( @@ -416,6 +456,11 @@ fun buildSQLMetadataValidationErrorChatContent(errorReason: String) = CodeTransf message = errorReason, ) +fun buildCustomDependencyVersionsFileInvalidChatContent() = CodeTransformChatMessageContent( + type = CodeTransformChatMessageType.FinalizedAnswer, + message = "The file you uploaded does not follow the format of the sample YAML file provided.", +) + fun buildUserCancelledChatContent() = CodeTransformChatMessageContent( type = CodeTransformChatMessageType.FinalizedAnswer, message = message("codemodernizer.chat.message.transform_cancelled_by_user"), @@ -451,6 +496,11 @@ fun buildUserLanguageUpgradeSelectionSummaryChatContent(moduleName: String, targ message = getUserLanguageUpgradeSelectionFormattedMarkdown(moduleName, targetJdkVersion) ) +fun buildContinueTransformationChatContent() = CodeTransformChatMessageContent( + type = CodeTransformChatMessageType.FinalizedAnswer, + message = "Ok, I will continue without this information.", +) + fun buildCompileLocalInProgressChatContent() = CodeTransformChatMessageContent( type = CodeTransformChatMessageType.PendingAnswer, message = message("codemodernizer.chat.message.local_build_begin"), diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt index b292aaf6aaa..7e1f17ce257 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/controller/CodeTransformChatController.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.projectRoots.JavaSdkVersion +import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.util.io.FileUtil.createTempDirectory import com.intellij.openapi.vfs.VirtualFile import kotlinx.coroutines.delay @@ -43,6 +44,9 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCo import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCompileLocalFailedNoJdkChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCompileLocalInProgressChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCompileLocalSuccessChatContent +import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildContinueTransformationChatContent +import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCustomDependencyVersionsFileInvalidChatContent +import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCustomDependencyVersionsFileValidChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildDownloadFailureChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHilCannotResumeContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHilErrorContent @@ -50,11 +54,12 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHi import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHilRejectContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHilResumeWithErrorContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHilResumedContent +import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildInvalidTargetJdkNameChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildLanguageUpgradeProjectValidChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildModuleSchemaFormChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildModuleSchemaFormIntroChatContent -import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildObjectiveChosenChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildProjectInvalidChatContent +import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildPromptTargetJDKNameChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildSQLMetadataValidationErrorChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildSQLMetadataValidationSuccessDetailsChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildSQLMetadataValidationSuccessIntroChatContent @@ -79,6 +84,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildUs import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildUserInputSkipTestsFlagChatIntroContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildUserLanguageUpgradeSelectionSummaryChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildUserOneOrMultipleDiffsSelectionChatContent +import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildUserReplyChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildUserSQLConversionSelectionSummaryChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildUserSkipTestsFlagSelectionChatContent import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildUserStopTransformChatContent @@ -88,6 +94,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.messages.CodeTran import software.aws.toolkits.jetbrains.services.codemodernizer.messages.IncomingCodeTransformMessage import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerArtifact import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerJobCompletedResult +import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformConversationState import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformHilDownloadArtifact import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformType import software.aws.toolkits.jetbrains.services.codemodernizer.model.CustomerSelection @@ -111,6 +118,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTrans import software.aws.toolkits.jetbrains.services.codemodernizer.utils.toVirtualFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.tryGetJdk import software.aws.toolkits.jetbrains.services.codemodernizer.utils.unzipFile +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.validateCustomVersionsFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.validateSctMetadata import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent @@ -130,9 +138,16 @@ class CodeTransformChatController( private val telemetry = CodeTransformTelemetryManager.getInstance(context.project) override suspend fun processChatPromptMessage(message: IncomingCodeTransformMessage.ChatPrompt) { + if (chatSessionStorage.getSession(message.tabId).conversationState == CodeTransformConversationState.PROMPT_TARGET_JDK_NAME) { + // we are prompting user for target JDK name + processJDKNameChatPromptMessage(message) + return + } + + // otherwise, we are asking for transformation objective val objective = message.message.trim().lowercase() - codeTransformChatHelper.addNewMessage(buildObjectiveChosenChatContent(objective)) + codeTransformChatHelper.addNewMessage(buildUserReplyChatContent(objective)) codeTransformChatHelper.sendChatInputEnabledMessage(message.tabId, false) codeTransformChatHelper.sendUpdatePlaceholderMessage(message.tabId, "Open a new tab to chat with Q") @@ -186,6 +201,7 @@ class CodeTransformChatController( } private suspend fun getUserObjective(tabId: String) { + chatSessionStorage.getSession(tabId).conversationState = CodeTransformConversationState.PROMPT_OBJECTIVE codeTransformChatHelper.addNewMessage(buildChooseTransformationObjectiveChatContent()) codeTransformChatHelper.sendChatInputEnabledMessage(tabId, true) codeTransformChatHelper.sendUpdatePlaceholderMessage(tabId, message("codemodernizer.chat.message.choose_objective_placeholder")) @@ -353,7 +369,6 @@ class CodeTransformChatController( withContext(EDT) { val descriptor = FileChooserDescriptorFactory.createSingleFileDescriptor() .withDescription("Select metadata file") - .withFileFilter { it.extension == "zip" } val selectedZipFile = FileChooser.chooseFile(descriptor, null, null) ?: return@withContext val extractedZip = createTempDirectory("codeTransformSQLMetadata", null) @@ -412,21 +427,105 @@ class CodeTransformChatController( override suspend fun processCodeTransformOneOrMultipleDiffs(message: IncomingCodeTransformMessage.CodeTransformConfirmOneOrMultipleDiffs) { val transformCapabilities = when (message.oneOrMultipleDiffsSelection) { + // TODO: add CLIENT_SIDE_BUILD to both below when releasing CSB message("codemodernizer.chat.message.one_or_multiple_diffs_form.multiple_diffs") -> listOf( EXPLAINABILITY_V1, SELECTIVE_TRANSFORMATION_V1 ) else -> listOf( - EXPLAINABILITY_V1 + EXPLAINABILITY_V1, ) } telemetry.submitSelection(message.oneOrMultipleDiffsSelection) codeTransformChatHelper.addNewMessage(buildUserOneOrMultipleDiffsSelectionChatContent(message.oneOrMultipleDiffsSelection)) - codeTransformChatHelper.addNewMessage(buildCompileLocalInProgressChatContent()) codeModernizerManager.codeTransformationSession?.let { it.sessionContext.transformCapabilities = transformCapabilities codeModernizerManager.runLocalMavenBuild(context.project, it) } + // TODO: when releasing CSB, delete "runLocalMavenBuild" line above and uncomment line below + // promptForCustomYamlFile() + } + + override suspend fun processCodeTransformCustomDependencyVersions(message: IncomingCodeTransformMessage.CodeTransformConfirmCustomDependencyVersions) { + withContext(EDT) { + val descriptor = FileChooserDescriptorFactory.createSingleFileDescriptor() + .withDescription("Select .yaml file") + val selectedFile = FileChooser.chooseFile(descriptor, null, null) ?: return@withContext + val isValid = validateCustomVersionsFile(selectedFile) + if (!isValid) { + codeTransformChatHelper.updateLastPendingMessage(buildCustomDependencyVersionsFileInvalidChatContent()) + codeTransformChatHelper.addNewMessage(buildStartNewTransformFollowup()) + return@withContext + } + codeModernizerManager.codeTransformationSession?.let { + it.sessionContext.customDependencyVersionsFile = selectedFile + } + codeTransformChatHelper.updateLastPendingMessage(buildCustomDependencyVersionsFileValidChatContent()) + promptForTargetJdkName(message.tabId) + } + } + + private suspend fun processJDKNameChatPromptMessage(message: IncomingCodeTransformMessage.ChatPrompt) { + chatSessionStorage.getSession(message.tabId).conversationState = CodeTransformConversationState.IDLE + codeTransformChatHelper.sendChatInputEnabledMessage(message.tabId, false) + codeTransformChatHelper.sendUpdatePlaceholderMessage(message.tabId, "") + + val providedJdkName = message.message.trim().lowercase() + val targetJdkName = ProjectJdkTable.getInstance().allJdks.find { it.name.trim().lowercase() == providedJdkName }?.name + if (targetJdkName == null) { + codeTransformChatHelper.addNewMessage(buildInvalidTargetJdkNameChatContent(providedJdkName)) + return + } + codeModernizerManager.codeTransformationSession?.sessionContext?.targetJdkName = targetJdkName + codeTransformChatHelper.addNewMessage(buildUserReplyChatContent(message.message.trim())) + // start local build once we get target JDK path + codeTransformChatHelper.addNewMessage(buildCompileLocalInProgressChatContent()) + codeModernizerManager.codeTransformationSession?.let { + codeModernizerManager.runLocalMavenBuild(context.project, it) + } + } + + // TODO: uncomment when releasing CSB +/* + private suspend fun promptForCustomYamlFile() { + codeTransformChatHelper.addNewMessage(buildUserInputCustomDependencyVersionsChatContent()) + val sampleYAML = """ +name: "custom-dependency-management" +description: "Custom dependency version management for Java migration from JDK 8/11/17 to JDK 17/21" + +dependencyManagement: + dependencies: + - identifier: "com.example:library1" + targetVersion: "2.1.0" + versionProperty: "library1.version" # Optional + originType: "FIRST_PARTY" # or "THIRD_PARTY" # Optional + - identifier: "com.example:library2" + targetVersion: "3.0.0" + originType: "THIRD_PARTY" + plugins: + - identifier: "com.example.plugin" + targetVersion: "1.2.0" + versionProperty: "plugin.version" # Optional + """.trimIndent() + + val virtualFile = LightVirtualFile("sample-dependency-management.yaml", YAMLFileType.YML, sampleYAML) + virtualFile.isWritable = true + ApplicationManager.getApplication().invokeLater { + FileEditorManager.getInstance(context.project).openFile(virtualFile, true) + } + } +*/ + override suspend fun processCodeTransformContinueAction(message: IncomingCodeTransformMessage.CodeTransformContinue) { + codeTransformChatHelper.addNewMessage(buildContinueTransformationChatContent()) + promptForTargetJdkName(message.tabId) + } + + private suspend fun promptForTargetJdkName(tabId: String) { + chatSessionStorage.getSession(tabId).conversationState = CodeTransformConversationState.PROMPT_TARGET_JDK_NAME + val targetJdkVersion = codeModernizerManager.codeTransformationSession?.sessionContext?.targetJavaVersion?.name.orEmpty() + codeTransformChatHelper.addNewMessage(buildPromptTargetJDKNameChatContent(targetJdkVersion)) + codeTransformChatHelper.sendChatInputEnabledMessage(tabId, true) + codeTransformChatHelper.sendUpdatePlaceholderMessage(tabId, "Enter the name of your $targetJdkVersion") } private fun getSourceJdk(moduleConfigurationFile: VirtualFile): JavaSdkVersion { diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/MavenRunnerUtils.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/MavenRunnerUtils.kt index 6629b8c2d34..35ecb95564e 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/MavenRunnerUtils.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/MavenRunnerUtils.kt @@ -4,19 +4,23 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile import org.jetbrains.idea.maven.execution.MavenRunner import org.jetbrains.idea.maven.execution.MavenRunnerParameters import org.jetbrains.idea.maven.execution.MavenRunnerSettings import org.slf4j.Logger import software.aws.toolkits.core.utils.error import software.aws.toolkits.core.utils.info +import software.aws.toolkits.jetbrains.services.codemodernizer.CodeModernizerManager import software.aws.toolkits.jetbrains.services.codemodernizer.CodeTransformTelemetryManager import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerSessionContext +import software.aws.toolkits.jetbrains.services.codemodernizer.model.MAVEN_BUILD_RUN_UNIT_TESTS import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenCopyCommandsResult import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenDependencyReportCommandsResult import software.aws.toolkits.telemetry.CodeTransformBuildCommand @@ -25,6 +29,27 @@ import java.io.File import java.nio.file.Files import java.nio.file.Path +fun runClientSideBuild(targetDir: VirtualFile, logger: Logger, project: Project): Pair { + // run mvn test-compile or mvn test + val transformMvnRunner = TransformMavenRunner(project) + val mvnSettings = MavenRunner.getInstance(project).settings.clone() + val buildRunnable = runClientSideBuild(targetDir, mvnSettings, transformMvnRunner, logger, project) + buildRunnable.await() + // write build output to a new text file and open it + val buildLogOutput = buildRunnable.getOutput().toString() + // first line is a long Maven String showing the build command; not useful or needed + val outputWithoutFirstLine = buildLogOutput.lines().drop(1).joinToString("\n") + val buildLogOutputFile = Files.createTempFile("build-logs-", ".txt") + Files.write(buildLogOutputFile, outputWithoutFirstLine.toByteArray()) + val buildLogOutputVirtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(buildLogOutputFile.toFile()) + runInEdt { + if (buildLogOutputVirtualFile != null) { + FileEditorManager.getInstance(project).openFile(buildLogOutputVirtualFile, true) + } + } + return buildRunnable.getExitCode() to buildRunnable.getOutput().toString() +} + fun runHilMavenCopyDependency( context: CodeModernizerSessionContext, sourceFolder: File, @@ -80,7 +105,6 @@ fun runMavenCopyCommands( val transformMvnRunner = TransformMavenRunner(project) context.mavenRunnerQueue.add(transformMvnRunner) val mvnSettings = MavenRunner.getInstance(project).settings.clone() // clone required to avoid editing user settings - val sourceVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(sourceFolder) val module = sourceVirtualFile?.let { ModuleUtilCore.findModuleForFile(it, project) } val moduleSdk = module?.let { ModuleRootManager.getInstance(it).sdk } @@ -191,6 +215,36 @@ private fun runMavenCopyDependencies( return copyTransformRunnable } +private fun runClientSideBuild( + targetDir: VirtualFile, + mvnSettings: MavenRunnerSettings, + transformMavenRunner: TransformMavenRunner, + logger: Logger, + project: Project, +): TransformRunnable { + val customBuildCommand = CodeModernizerManager.getInstance(project).codeTransformationSession?.sessionContext?.customBuildCommand + val clientSideBuildCommand = if (customBuildCommand == MAVEN_BUILD_RUN_UNIT_TESTS) "test" else "test-compile" + val buildParams = MavenRunnerParameters( + false, + targetDir.path, + null, + listOf(clientSideBuildCommand), + null, + null + ) + val buildTransformRunnable = TransformRunnable() + runInEdt { + try { + CodeModernizerManager.getInstance(project).getMvnBuildWindow().show() + transformMavenRunner.run(buildParams, mvnSettings, buildTransformRunnable, true) + } catch (t: Throwable) { + logger.error(t) { "Maven Build: Unexpected error when executing bundled Maven $clientSideBuildCommand" } + buildTransformRunnable.setExitCode(Integer.MIN_VALUE) // to stop looking for the exitCode + } + } + return buildTransformRunnable +} + private fun runMavenClean( sourceFolder: File, buildlogBuilder: StringBuilder, @@ -205,7 +259,7 @@ private fun runMavenClean( sourceFolder.absolutePath, null, listOf("-Dmaven.repo.local=$destinationDir", "clean"), - emptyList(), + null, null ) val cleanTransformRunnable = TransformRunnable() @@ -241,7 +295,7 @@ private fun runMavenInstall( sourceFolder.absolutePath, null, flags, - emptyList(), + null, null ) val installTransformRunnable = TransformRunnable() @@ -277,7 +331,7 @@ private fun runMavenDependencyUpdatesReport( sourceFolder.absolutePath, null, dependencyUpdatesReportCommandList, - emptyList(), + null, null ) val dependencyUpdatesReportRunnable = TransformRunnable() diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformMavenRunner.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformMavenRunner.kt index a68c659ba6f..4590c529793 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformMavenRunner.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformMavenRunner.kt @@ -6,7 +6,6 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven import com.intellij.execution.process.ProcessAdapter import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessHandler -import com.intellij.execution.process.ProcessOutputTypes import com.intellij.execution.runners.ProgramRunner import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.fileEditor.FileDocumentManager @@ -15,17 +14,22 @@ import com.intellij.openapi.util.Key import org.jetbrains.idea.maven.execution.MavenRunConfigurationType import org.jetbrains.idea.maven.execution.MavenRunnerParameters import org.jetbrains.idea.maven.execution.MavenRunnerSettings +import software.aws.toolkits.jetbrains.services.codemodernizer.CodeModernizerManager class TransformMavenRunner(val project: Project) { private var handler: ProcessHandler? = null + fun run(parameters: MavenRunnerParameters, settings: MavenRunnerSettings, onComplete: TransformRunnable, isClientSideBuild: Boolean = false) { + if (isClientSideBuild) { + val targetJdkName = CodeModernizerManager.getInstance(project).codeTransformationSession?.sessionContext?.targetJdkName + ?: throw RuntimeException("Could not find user's target JDK; cannot run client-side build") // should not happen; already validated earlier + settings.setJreName(targetJdkName) + } - fun run(parameters: MavenRunnerParameters, settings: MavenRunnerSettings, onComplete: TransformRunnable) { FileDocumentManager.getInstance().saveAllDocuments() val callback = ProgramRunner.Callback { descriptor: RunContentDescriptor -> val handler = descriptor.processHandler this.handler = handler if (handler == null) { - // add log error here onComplete.setExitCode(-1) return@Callback } @@ -33,13 +37,9 @@ class TransformMavenRunner(val project: Project) { var output: String = "" override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { - when (outputType) { - ProcessOutputTypes.STDOUT -> { - output += event.text - } - ProcessOutputTypes.STDERR -> { - output += event.text - } + // IntelliJ includes some unneeded lines in stdout; exclude those from build logs + if (!event.text.startsWith("[IJ]")) { + output += event.text } } @@ -49,8 +49,6 @@ class TransformMavenRunner(val project: Project) { } }) } - // Change runner from IntelliJ controlled to Maven controlled - // Setting isDelegateBuild = true allows us to set the JRE used by Maven during runtime MavenRunConfigurationType.runConfiguration(project, parameters, null, settings, callback, false) } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformRunnable.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformRunnable.kt index c4ab0e6fc44..cc5307b6062 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformRunnable.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformRunnable.kt @@ -11,6 +11,8 @@ class TransformRunnable : Runnable { exitCode = i } + fun getExitCode(): Int? = exitCode + override fun run() { // do nothing } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/messages/CodeTransformMessage.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/messages/CodeTransformMessage.kt index 9a32e82aff5..b26fcbaeae2 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/messages/CodeTransformMessage.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/messages/CodeTransformMessage.kt @@ -20,8 +20,10 @@ enum class CodeTransformButtonId(val id: String) { SelectSQLMetadata("codetransform-input-select-sql-metadata"), SelectSQLModuleSchema("codetransform-input-select-sql-module-schema"), CancelTransformation("codetransform-input-cancel"), + ContinueTransformation("codetransform-input-continue"), ConfirmSkipTests("codetransform-input-confirm-skip-tests"), ConfirmOneOrMultipleDiffs("codetransform-input-confirm-one-or-multiple-diffs"), + ConfirmCustomDependencyVersions("codetransform-input-confirm-custom-dependency-versions"), StopTransformation("stop_transform"), OpenTransformationHub("open_transformation_hub"), OpenMvnBuild("open_mvn_build"), @@ -103,6 +105,10 @@ sealed interface IncomingCodeTransformMessage : CodeTransformBaseMessage { @JsonProperty("tabID") val tabId: String, ) : IncomingCodeTransformMessage + data class CodeTransformContinue( + @JsonProperty("tabID") val tabId: String, + ) : IncomingCodeTransformMessage + data class CodeTransformConfirmSkipTests( @JsonProperty("tabID") val tabId: String, val skipTestsSelection: String, @@ -113,6 +119,10 @@ sealed interface IncomingCodeTransformMessage : CodeTransformBaseMessage { val oneOrMultipleDiffsSelection: String, ) : IncomingCodeTransformMessage + data class CodeTransformConfirmCustomDependencyVersions( + @JsonProperty("tabID") val tabId: String, + ) : IncomingCodeTransformMessage + data class CodeTransformOpenMvnBuild( @JsonProperty("tabID") val tabId: String, ) : IncomingCodeTransformMessage diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/BuildProgressTimelineStepDetailsList.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/BuildProgressTimelineStepDetailsList.kt index 77f15eea0c3..1409559b139 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/BuildProgressTimelineStepDetailsList.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/BuildProgressTimelineStepDetailsList.kt @@ -25,7 +25,7 @@ fun getTransformationProgressStepsByTransformationStepId( if (progressStep != null) { val itemToAdd = BuildProgressTimelineStepDetailItem( progressStep.name(), - progressStep.description(), + progressStep.description().orEmpty(), mapTransformationPlanApiStatus(progressStep.status()), progressStep.startTime()?.toString(), progressStep.endTime()?.toString() diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeModernizerSessionContext.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeModernizerSessionContext.kt index a0da0824a76..93bf8a20b1a 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeModernizerSessionContext.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeModernizerSessionContext.kt @@ -17,7 +17,6 @@ import software.aws.toolkits.core.utils.error import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.putNextEntry -import software.aws.toolkits.jetbrains.services.codemodernizer.EXPLAINABILITY_V1 import software.aws.toolkits.jetbrains.services.codemodernizer.constants.HIL_DEPENDENCIES_ROOT_NAME import software.aws.toolkits.jetbrains.services.codemodernizer.constants.HIL_MANIFEST_FILE_NAME import software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven.TransformMavenRunner @@ -46,9 +45,11 @@ const val MANIFEST_PATH = "manifest.json" const val ZIP_SOURCES_PATH = "sources" const val ZIP_DEPENDENCIES_PATH = "dependencies" const val BUILD_LOG_PATH = "build-logs.txt" +const val CUSTOM_DEPENDENCY_VERSIONS_FILE_PATH = "custom-upgrades.yaml" const val UPLOAD_ZIP_MANIFEST_VERSION = "1.0" const val HIL_1P_UPGRADE_CAPABILITY = "HIL_1pDependency_VersionUpgrade" const val EXPLAINABILITY_V1 = "EXPLAINABILITY_V1" +const val CLIENT_SIDE_BUILD = "CLIENT_SIDE_BUILD" const val MAVEN_CONFIGURATION_FILE_NAME = "pom.xml" const val MAVEN_BUILD_RUN_UNIT_TESTS = "clean test" const val MAVEN_BUILD_SKIP_UNIT_TESTS = "clean test-compile" @@ -67,13 +68,16 @@ data class CodeModernizerSessionContext( var configurationFile: VirtualFile? = null, // used to ZIP module val sourceJavaVersion: JavaSdkVersion, // always needed for startJob API val targetJavaVersion: JavaSdkVersion, // 17 or 21 - var transformCapabilities: List = listOf(EXPLAINABILITY_V1), + var transformCapabilities: List = listOf(), var customBuildCommand: String = MAVEN_BUILD_RUN_UNIT_TESTS, // run unit tests by default val sourceVendor: String = ORACLE_DB, // only one supported val targetVendor: String? = null, val sourceServerName: String? = null, var schema: String? = null, val sqlMetadataZip: File? = null, + var customDependencyVersionsFile: VirtualFile? = null, + var targetJdkName: String? = null, + var originalUploadZipPath: Path? = null, ) : Disposable { private val mapper = jacksonObjectMapper() private val ignoredDependencyFileExtensions = setOf(INVALID_SUFFIX_SHA, INVALID_SUFFIX_REPOSITORIES) @@ -268,7 +272,15 @@ data class CodeModernizerSessionContext( LOG.info { "Dependency files size = ${dependencyFiles.sumOf { it.length().toInt() }}" } - // 3) Sources + // 3) Custom YAML file + // TODO: where to put this? VS Code puts it in custom-upgrades/dependency-versions.yaml; here we put it at the root + if (customDependencyVersionsFile != null) { + customDependencyVersionsFile?.inputStream?.use { + zip.putNextEntry(Path(CUSTOM_DEPENDENCY_VERSIONS_FILE_PATH).toString(), it) + } + } + + // 4) Sources files?.forEach { file -> val relativePath = File(file.path).relativeTo(sourceFolder) val paddedPath = zipSources.resolve(relativePath) @@ -289,7 +301,7 @@ data class CodeModernizerSessionContext( LOG.info { "Source code files size = ${files?.sumOf { it.length.toInt() }}" } - // 4) Build Log + // 5) Initial Maven copy-deps / install build log buildLogBuilder.toString().byteInputStream().use { zip.putNextEntry(Path(BUILD_LOG_PATH).toString(), it) } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeTransformConversationState.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeTransformConversationState.kt new file mode 100644 index 00000000000..0d4c73c208f --- /dev/null +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeTransformConversationState.kt @@ -0,0 +1,10 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codemodernizer.model + +enum class CodeTransformConversationState { + PROMPT_OBJECTIVE, + PROMPT_TARGET_JDK_NAME, + IDLE, +} diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CustomerSelection.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CustomerSelection.kt index 151187c165a..5c9828a1e0e 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CustomerSelection.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CustomerSelection.kt @@ -15,5 +15,7 @@ data class CustomerSelection( val targetVendor: String? = null, val sourceServerName: String? = null, val sqlMetadataZip: File? = null, - // note: schema and customBuildCommand are passed in to CodeModernizerSessionContext separately, *after* CodeModernizerSession is created + // note: schema / customBuildCommand / customDependencyVersionsFile / targetJdkName / originalUploadZipPath + // are passed in to CodeModernizerSessionContext separately, + // *after* CodeModernizerSession is created ) diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/ZipManifest.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/ZipManifest.kt index 9267de9ddb9..e65b08f7e12 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/ZipManifest.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/ZipManifest.kt @@ -3,12 +3,14 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.model +// TODO: include custom yaml file path in manifest.json? data class ZipManifest( val sourcesRoot: String = ZIP_SOURCES_PATH, val dependenciesRoot: String = ZIP_DEPENDENCIES_PATH, val buildLogs: String = BUILD_LOG_PATH, val version: String = UPLOAD_ZIP_MANIFEST_VERSION, val hilCapabilities: List = listOf(HIL_1P_UPGRADE_CAPABILITY), + // TODO: add CLIENT_SIDE_BUILD to transformCapabilities when releasing CSB val transformCapabilities: List = listOf(EXPLAINABILITY_V1), val customBuildCommand: String = MAVEN_BUILD_RUN_UNIT_TESTS, val requestedConversions: RequestedConversions? = null, // only used for SQL conversions for now diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/panels/BuildProgressStepDetailsPanel.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/panels/BuildProgressStepDetailsPanel.kt index 28bd980c1aa..0f60b3ceb23 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/panels/BuildProgressStepDetailsPanel.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/panels/BuildProgressStepDetailsPanel.kt @@ -72,7 +72,7 @@ class BuildProgressStepDetailsPanel : JPanel(BorderLayout()) { it.description } else { if (it.status == BuildStepStatus.DONE) { - message("codemodernizer.migration_plan.substeps.description_succeed") + message("codemodernizer.migration_plan.substeps.description_completed") } else if (it.status == BuildStepStatus.ERROR) { message("codemodernizer.migration_plan.substeps.description_failed") } else { diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/session/Session.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/session/Session.kt index f32c78f206c..550d3205705 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/session/Session.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/session/Session.kt @@ -3,9 +3,12 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.session +import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformConversationState + data class Session( val tabId: String, ) { var isAuthenticating: Boolean = false var authNeededNotified: Boolean = false + var conversationState: CodeTransformConversationState = CodeTransformConversationState.IDLE } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt index 9ddc086d819..6495508328b 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt @@ -5,9 +5,20 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.utils import com.fasterxml.jackson.module.kotlin.readValue import com.intellij.notification.NotificationAction +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.diff.impl.patch.PatchReader +import com.intellij.openapi.diff.impl.patch.formove.PatchApplier +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.module.JavaModuleType +import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.util.io.FileUtil.createTempDirectory +import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.serviceContainer.AlreadyDisposedException import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext import software.amazon.awssdk.awscore.exception.AwsServiceException import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException @@ -18,21 +29,32 @@ import software.amazon.awssdk.services.codewhispererruntime.model.Transformation import software.amazon.awssdk.services.codewhispererruntime.model.TransformationPlan import software.amazon.awssdk.services.codewhispererruntime.model.TransformationProgressUpdate import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStatus +import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStep +import software.amazon.awssdk.services.codewhispererruntime.model.TransformationUploadContext +import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext import software.amazon.awssdk.services.codewhispererruntime.model.ValidationException import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException import software.aws.toolkits.core.utils.WaiterUnrecoverableException import software.aws.toolkits.core.utils.Waiters.waitUntil +import software.aws.toolkits.core.utils.error +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.info +import software.aws.toolkits.jetbrains.core.coroutines.EDT +import software.aws.toolkits.jetbrains.services.codemodernizer.CodeModernizerManager import software.aws.toolkits.jetbrains.services.codemodernizer.CodeTransformTelemetryManager import software.aws.toolkits.jetbrains.services.codemodernizer.client.GumbyClient import software.aws.toolkits.jetbrains.services.codemodernizer.commands.CodeTransformMessageListener import software.aws.toolkits.jetbrains.services.codemodernizer.constants.BILLING_RATE import software.aws.toolkits.jetbrains.services.codemodernizer.constants.JOB_STATISTICS_TABLE_KEY +import software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven.runClientSideBuild import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerArtifact.Companion.MAPPER import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformType import software.aws.toolkits.jetbrains.services.codemodernizer.model.JobId import software.aws.toolkits.jetbrains.services.codemodernizer.model.PlanTable import software.aws.toolkits.jetbrains.utils.notifyStickyWarn import software.aws.toolkits.resources.message +import java.nio.file.Path +import java.nio.file.Paths import java.time.Duration import java.util.Locale import java.util.concurrent.atomic.AtomicBoolean @@ -44,6 +66,8 @@ data class PollingResult( val transformationPlan: TransformationPlan?, ) +private const val IS_CLIENT_SIDE_BUILD_ENABLED = false + /** * Wrapper around [waitUntil] that polls the API DescribeMigrationJob to check the migration job status. */ @@ -97,6 +121,10 @@ suspend fun JobId.pollTransformationStatusAndPlan( delay(sleepDurationMillis) newPlan = clientAdaptor.getCodeModernizationPlan(this).transformationPlan() } + // TODO: remove flag when releasing CSB + if (IS_CLIENT_SIDE_BUILD_ENABLED && newStatus == TransformationStatus.TRANSFORMING && newPlan != null) { + attemptLocalBuild(newPlan, this, project) + } if (newStatus != state) { telemetry.jobStatusChanged(this, newStatus.toString(), state.toString()) } @@ -144,6 +172,119 @@ suspend fun JobId.pollTransformationStatusAndPlan( return PollingResult(true, transformationResponse?.transformationJob(), state, transformationPlan) } +suspend fun attemptLocalBuild(plan: TransformationPlan, jobId: JobId, project: Project) { + val artifactId = getClientInstructionArtifactId(plan) + getLogger().info { "Found artifactId: $artifactId" } + if (artifactId != null) { + val clientInstructionsPath = downloadClientInstructions(jobId, artifactId, project) + getLogger().info { "Downloaded client instructions for job ${jobId.id} and artifact $artifactId at: $clientInstructionsPath" } + processClientInstructions(clientInstructionsPath, jobId, artifactId, project) + getLogger().info { "Finished processing client instructions for job ${jobId.id} and artifact $artifactId" } + } +} + +suspend fun processClientInstructions(clientInstructionsPath: Path, jobId: JobId, artifactId: String, project: Project) { + var copyOfProjectSources = createTempDirectory("originalCopy_${jobId.id}_$artifactId", null).toPath() + getLogger().info { "About to copy the original project ZIP to: $copyOfProjectSources" } + val originalProjectZip = CodeModernizerManager.getInstance(project).codeTransformationSession?.sessionContext?.originalUploadZipPath + originalProjectZip?.let { unzipFile(it, copyOfProjectSources, isSqlMetadata = false, extractOnlySources = true) } + copyOfProjectSources = copyOfProjectSources.resolve("sources") // where the user's source code is within the upload ZIP + getLogger().info { "Copied and unzipped original project sources to: $copyOfProjectSources" } + + val targetDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(copyOfProjectSources.toFile()) + ?: throw RuntimeException("Cannot find copy of project sources directory") + + withContext(EDT) { + runWriteAction { + // create temp module with project copy so that we can apply diff.patch + val modifiableModel = ModuleManager.getInstance(project).getModifiableModel() + val tempModule = modifiableModel.newModule( + Paths.get(targetDir.path).resolve("temp.iml").toString(), + JavaModuleType.getModuleType().id + ) + + try { + val moduleModel = ModuleRootManager.getInstance(tempModule).modifiableModel + moduleModel.addContentEntry(targetDir.url) + moduleModel.commit() + modifiableModel.commit() + + // apply diff.patch + val patchReader = PatchReader(clientInstructionsPath) + patchReader.parseAllPatches() + PatchApplier( + project, + targetDir, + patchReader.allPatches, + null, + null + ).execute() + getLogger().info { "Successfully applied patch file at $clientInstructionsPath" } + + val virtualFile = LocalFileSystem.getInstance().findFileByIoFile(clientInstructionsPath.toFile()) + ?: throw RuntimeException("Cannot find patch file at $clientInstructionsPath") + FileEditorManager.getInstance(project).openFile(virtualFile, true) + } catch (e: Exception) { + getLogger().error { + "Error applying intermediate diff.patch for job ${jobId.id} and artifact $artifactId located at " + + "$clientInstructionsPath: $e" + } + } finally { + runWriteAction { + ModuleManager.getInstance(project).disposeModule(tempModule) + } + } + } + } + + val (exitCode, buildOutput) = runClientSideBuild(targetDir, CodeModernizerManager.LOG, project) + getLogger().info { "Ran client-side build with an exit code of $exitCode" } + val uploadZip = createClientSideBuildUploadZip(exitCode, buildOutput) + getLogger().info { "Created client-side build result upload zip for job ${jobId.id} and artifact $artifactId: ${uploadZip.path}" } + val uploadContext = UploadContext.fromTransformationUploadContext( + TransformationUploadContext.builder().jobId(jobId.id).uploadArtifactType("ClientBuildResult").build() + ) + getLogger().info { "About to call uploadPayload for job ${jobId.id} and artifact $artifactId" } + try { + CodeModernizerManager.getInstance(project).codeTransformationSession?.uploadPayload(uploadZip, uploadContext) + getLogger().info { "Upload succeeded; about to call ResumeTransformation for job ${jobId.id} and artifact $artifactId now" } + CodeModernizerManager.getInstance(project).codeTransformationSession?.resumeTransformation() + } finally { + uploadZip.deleteRecursively() + copyOfProjectSources.toFile().deleteRecursively() + getLogger().info { "Deleted copy of project sources and client-side build upload ZIP" } + } + // switch back to Transformation Hub view + runInEdt { + CodeModernizerManager.getInstance(project).getBottomToolWindow().show() + } +} + +suspend fun downloadClientInstructions(jobId: JobId, artifactId: String, project: Project): Path { + val exportDestination = "downloadClientInstructions_${jobId.id}_$artifactId" + val exportZipPath = createTempDirectory(exportDestination, null) + val client = GumbyClient.getInstance(project) + val downloadBytes = client.downloadExportResultArchive(jobId, artifactId) + val downloadZipPath = zipToPath(downloadBytes, exportZipPath.toPath()).first.toAbsolutePath() + unzipFile(downloadZipPath, exportZipPath.toPath()) + return exportZipPath.toPath().resolve("diff.patch") +} + +fun getClientInstructionArtifactId(plan: TransformationPlan): String? { + val steps = plan.transformationSteps().drop(1) + val progressUpdate = findDownloadArtifactProgressUpdate(steps) + val artifactId = progressUpdate?.downloadArtifacts()?.firstOrNull()?.downloadArtifactId() + return artifactId +} + +fun findDownloadArtifactProgressUpdate(transformationSteps: List) = + transformationSteps + .flatMap { it.progressUpdates().orEmpty() } + .firstOrNull { update -> + update.status().name == "AWAITING_CLIENT_ACTION" && + update.downloadArtifacts()?.firstOrNull()?.downloadArtifactId() != null + } + // "name" holds the ID of the corresponding plan step (where table will go) and "description" holds the plan data fun getTableMapping(stepZeroProgressUpdates: List): Map { if (stepZeroProgressUpdates.isNotEmpty()) { diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformFileUtils.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformFileUtils.kt index bba84915c2b..ea19a954342 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformFileUtils.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformFileUtils.kt @@ -3,16 +3,22 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.utils +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.intellij.openapi.application.runReadAction import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.readText +import kotlinx.coroutines.withContext import software.aws.toolkits.core.utils.createParentDirectories +import software.aws.toolkits.core.utils.createTemporaryZipFile import software.aws.toolkits.core.utils.error import software.aws.toolkits.core.utils.exists import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info +import software.aws.toolkits.core.utils.putNextEntry +import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext import software.aws.toolkits.jetbrains.services.codemodernizer.CodeModernizerManager.Companion.LOG import software.aws.toolkits.jetbrains.services.codemodernizer.constants.HIL_ARTIFACT_DIR_NAME import software.aws.toolkits.jetbrains.services.codemodernizer.constants.HIL_ARTIFACT_POMFOLDER_DIR_NAME @@ -35,6 +41,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.content import software.aws.toolkits.resources.message import java.io.File import java.io.FileOutputStream +import java.nio.file.Files import java.nio.file.Path import java.util.zip.ZipFile import kotlin.io.path.Path @@ -60,6 +67,27 @@ fun filterOnlyParentFiles(filePaths: Set): List { return shortestRoots.toList() } +fun createClientSideBuildUploadZip(exitCode: Int?, stdout: String): File { + val mapper = jacksonObjectMapper() + val outputFile = createTemporaryZipFile { zip -> + val manifest = mapOf( + "capability" to "CLIENT_SIDE_BUILD", + "exitCode" to exitCode, + "commandLogFileName" to "build-output.log" + ) + mapper.writeValueAsString(manifest) + .byteInputStream() + .use { inputStream -> + zip.putNextEntry(Path("manifest.json").toString(), inputStream) + } + + stdout.byteInputStream().use { inputStream -> + zip.putNextEntry(Path("build-output.log").toString(), inputStream) + } + } + return outputFile.toFile() +} + /** * @description For every directory, check if any supported build files (pom.xml etc) exists. * If we find a valid build file, store it and stop further recursion. @@ -113,7 +141,7 @@ fun parseBuildFile(buildFile: VirtualFile?): String? { /** * Unzips a zip into a dir. Returns the true when successfully unzips the file pointed to by [zipFilePath] to [destDir] */ -fun unzipFile(zipFilePath: Path, destDir: Path, isSqlMetadata: Boolean = false): Boolean { +fun unzipFile(zipFilePath: Path, destDir: Path, isSqlMetadata: Boolean = false, extractOnlySources: Boolean = false): Boolean { if (!zipFilePath.exists()) return false val zipFile = ZipFile(zipFilePath.toFile()) zipFile.use { file -> @@ -121,6 +149,12 @@ fun unzipFile(zipFilePath: Path, destDir: Path, isSqlMetadata: Boolean = false): .filterNot { it.isDirectory } .map { zipEntry -> var fileName = zipEntry.name + if (extractOnlySources) { + // used to copy just the source code for client-side build + if (!fileName.startsWith("sources")) { + return@map + } + } if (isSqlMetadata) { // when manually compressing ZIP files, the files get unzipped under a subdirectory where we extract them to, // this change puts the files directly under the root of the target directory, which is what we want @@ -136,12 +170,48 @@ fun unzipFile(zipFilePath: Path, destDir: Path, isSqlMetadata: Boolean = false): return true } +suspend fun zipToPath(byteArrayList: List, outputDirPath: Path? = null): Pair { + val zipFilePath = withContext(getCoroutineBgContext()) { + if (outputDirPath == null) { + Files.createTempFile(null, ".zip") + } else { + Files.createTempFile(outputDirPath, null, ".zip") + } + } + var totalDownloadBytes = 0 + withContext(getCoroutineBgContext()) { + Files.newOutputStream(zipFilePath).use { + for (bytes in byteArrayList) { + it.write(bytes) + totalDownloadBytes += bytes.size + } + } + } + return zipFilePath to totalDownloadBytes +} + fun parseXmlDependenciesReport(pathToXmlDependency: Path): DependencyUpdatesReport { val reportFile = pathToXmlDependency.toFile() val report = XML_MAPPER.readValue(reportFile, DependencyUpdatesReport::class.java) return report } +fun validateCustomVersionsFile(file: VirtualFile): Boolean { + if (!file.name.lowercase().endsWith(".yaml")) { + getLogger().error { "Custom versions file is not a YAML file: ${file.name}" } + return false + } + val fileContents = file.readText() + val requiredKeys = listOf("dependencyManagement:", "identifier:", "targetVersion:") + for (key in requiredKeys) { + if (!fileContents.contains(key)) { + getLogger().error { "Missing yaml key: $key" } + return false + } + } + return true +} + fun validateSctMetadata(sctFile: File?): SqlMetadataValidationResult { if (sctFile == null) { return SqlMetadataValidationResult(false, message("codemodernizer.chat.message.validation.error.missing_sct_file")) diff --git a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerSessionTest.kt b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerSessionTest.kt index 044cb72a757..72fef62a11e 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerSessionTest.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerSessionTest.kt @@ -26,6 +26,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import org.mockito.ArgumentMatchers.nullable import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.mockito.Mockito.spy @@ -45,6 +46,7 @@ import software.amazon.awssdk.awscore.util.AwsHeader import software.amazon.awssdk.http.SdkHttpResponse import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStatus +import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext import software.amazon.awssdk.services.ssooidc.model.SsoOidcException import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerJobCompletedResult @@ -410,12 +412,12 @@ class CodeWhispererCodeModernizerSessionTest : CodeWhispererCodeModernizerTestBa fun `CodeModernizer can create modernization job`() = runTest { doReturn(ZipCreationResult.Succeeded(File("./tst-resources/codemodernizer/test.txt"))) .whenever(testSessionContextSpy).createZipWithModuleFiles(any()) - doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any()) + doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) doNothing().whenever(clientAdaptorSpy).uploadArtifactToS3(any(), any(), any(), any(), any()) doReturn(exampleStartCodeMigrationResponse).whenever(clientAdaptorSpy).startCodeModernization(any(), any(), any()) val result = testSessionSpy.createModernizationJob(MavenCopyCommandsResult.Success(File("./mock/path/"))) assertThat(result).isEqualTo(CodeModernizerStartJobResult.Started(jobId)) - verify(clientAdaptorSpy, times(1)).createGumbyUploadUrl(any()) + verify(clientAdaptorSpy, times(1)).createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) verify(clientAdaptorSpy, times(1)).startCodeModernization(any(), any(), any()) verify(clientAdaptorSpy, times(1)).uploadArtifactToS3(any(), any(), any(), any(), any()) verifyNoMoreInteractions(clientAdaptorSpy) @@ -425,7 +427,7 @@ class CodeWhispererCodeModernizerSessionTest : CodeWhispererCodeModernizerTestBa fun `CodeModernizer cannot upload payload due to already disposed`() = runTest { doReturn(ZipCreationResult.Succeeded(File("./tst-resources/codemodernizer/test.txt"))) .whenever(testSessionContextSpy).createZipWithModuleFiles(any()) - doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any()) + doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) doAnswer { throw AlreadyDisposedException("mock exception") }.whenever(clientAdaptorSpy).uploadArtifactToS3(any(), any(), any(), any(), any()) val result = testSessionSpy.createModernizationJob(MavenCopyCommandsResult.Success(File("./mock/path/"))) assertThat(result).isEqualTo(CodeModernizerStartJobResult.Disposed) @@ -436,7 +438,7 @@ class CodeWhispererCodeModernizerSessionTest : CodeWhispererCodeModernizerTestBa setupConnection(BearerTokenAuthState.AUTHORIZED) doReturn(ZipCreationResult.Succeeded(File("./tst-resources/codemodernizer/test.txt"))) .whenever(testSessionContextSpy).createZipWithModuleFiles(any()) - doAnswer { throw SsoOidcException.builder().build() }.whenever(clientAdaptorSpy).createGumbyUploadUrl(any()) + doAnswer { throw SsoOidcException.builder().build() }.whenever(clientAdaptorSpy).createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) val result = testSessionSpy.createModernizationJob(MavenCopyCommandsResult.Success(File("./mock/path/"))) assertThat(result).isEqualTo(CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.CREDENTIALS_EXPIRED)) } @@ -454,8 +456,11 @@ class CodeWhispererCodeModernizerSessionTest : CodeWhispererCodeModernizerTestBa fun `CodeModernizer cannot upload payload due to presigned url issue`() = runTest { doReturn(ZipCreationResult.Succeeded(File("./tst-resources/codemodernizer/test.txt"))) .whenever(testSessionContextSpy).createZipWithModuleFiles(any()) - doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any()) - doAnswer { throw HttpRequests.HttpStatusException("mock error", 403, "mock url") }.whenever(testSessionSpy).uploadPayload(any()) + doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) + doAnswer { throw HttpRequests.HttpStatusException("mock error", 403, "mock url") }.whenever(testSessionSpy).uploadPayload( + any(), + nullable(UploadContext::class.java) + ) val result = testSessionSpy.createModernizationJob(MavenCopyCommandsResult.Success(File("./mock/path/"))) assertThat(result).isEqualTo(CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.PRESIGNED_URL_EXPIRED)) verify(testSessionStateSpy, times(1)).putJobHistory(any(), eq(TransformationStatus.FAILED), any(), any()) @@ -466,8 +471,11 @@ class CodeWhispererCodeModernizerSessionTest : CodeWhispererCodeModernizerTestBa fun `CodeModernizer cannot upload payload due to other status code`() = runTest { doReturn(ZipCreationResult.Succeeded(File("./tst-resources/codemodernizer/test.txt"))) .whenever(testSessionContextSpy).createZipWithModuleFiles(any()) - doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any()) - doAnswer { throw HttpRequests.HttpStatusException("mock error", 407, "mock url") }.whenever(testSessionSpy).uploadPayload(any()) + doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) + doAnswer { throw HttpRequests.HttpStatusException("mock error", 407, "mock url") }.whenever(testSessionSpy).uploadPayload( + any(), + nullable(UploadContext::class.java) + ) val result = testSessionSpy.createModernizationJob(MavenCopyCommandsResult.Success(File("./mock/path/"))) assertThat(result).isEqualTo(CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.HTTP_ERROR(407))) verify(testSessionStateSpy, times(1)).putJobHistory(any(), eq(TransformationStatus.FAILED), any(), any()) @@ -478,7 +486,7 @@ class CodeWhispererCodeModernizerSessionTest : CodeWhispererCodeModernizerTestBa fun `CodeModernizer cannot upload payload due to unknown client-side issue`() = runTest { doReturn(ZipCreationResult.Succeeded(File("./tst-resources/codemodernizer/test.txt"))) .whenever(testSessionContextSpy).createZipWithModuleFiles(any()) - doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any()) + doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) doAnswer { throw Exception("mock client-side exception") }.whenever(clientAdaptorSpy).uploadArtifactToS3(any(), any(), any(), any(), any()) val result = testSessionSpy.createModernizationJob(MavenCopyCommandsResult.Success(File("./mock/path/"))) assertThat(result).isEqualTo(CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.OTHER("mock client-side exception"))) @@ -490,8 +498,8 @@ class CodeWhispererCodeModernizerSessionTest : CodeWhispererCodeModernizerTestBa fun `CodeModernizer cannot upload payload due to connection refused`() = runTest { doReturn(ZipCreationResult.Succeeded(File("./tst-resources/codemodernizer/test.txt"))) .whenever(testSessionContextSpy).createZipWithModuleFiles(any()) - doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any()) - doAnswer { throw ConnectException("mock exception") }.whenever(testSessionSpy).uploadPayload(any()) + doReturn(exampleCreateUploadUrlResponse).whenever(clientAdaptorSpy).createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) + doAnswer { throw ConnectException("mock exception") }.whenever(testSessionSpy).uploadPayload(any(), nullable(UploadContext::class.java)) val result = testSessionSpy.createModernizationJob(MavenCopyCommandsResult.Success(File("./mock/path/"))) assertThat(result).isEqualTo(CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.CONNECTION_REFUSED)) verify(testSessionStateSpy, times(1)).putJobHistory(any(), eq(TransformationStatus.FAILED), any(), any()) @@ -558,14 +566,14 @@ class CodeWhispererCodeModernizerSessionTest : CodeWhispererCodeModernizerTestBa val expectedSha256checksum: String = Base64.getEncoder().encodeToString(DigestUtils.sha256(FileInputStream(expectedFilePath.toAbsolutePath().toString()))) clientAdaptorSpy.stub { - onGeneric { clientAdaptorSpy.createGumbyUploadUrl(any()) } + onGeneric { clientAdaptorSpy.createGumbyUploadUrl(any(), nullable(UploadContext::class.java)) } .thenReturn(gumbyUploadUrlResponse) } wireMock.stubFor(put(urlEqualTo("/")).willReturn(aResponse().withStatus(200))) - testSessionSpy.uploadPayload(expectedFilePath.toFile()) + testSessionSpy.uploadPayload(expectedFilePath.toFile(), null) val inOrder = inOrder(clientAdaptorSpy) - inOrder.verify(clientAdaptorSpy).createGumbyUploadUrl(eq(expectedSha256checksum)) + inOrder.verify(clientAdaptorSpy).createGumbyUploadUrl(eq(expectedSha256checksum), nullable(UploadContext::class.java)) inOrder.verify(clientAdaptorSpy).uploadArtifactToS3( eq(gumbyUploadUrlResponse.uploadUrl()), eq(expectedFilePath.toFile()), diff --git a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerTest.kt b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerTest.kt index 633879f4872..c5927c6d733 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerTest.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerTest.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.vcs.changes.patch.ApplyPatchDifferentiatedDialog import com.intellij.testFramework.LightVirtualFile import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test @@ -30,11 +31,17 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransfo import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadArtifactResult import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadFailureReason import software.aws.toolkits.jetbrains.services.codemodernizer.model.InvalidTelemetryReason +import software.aws.toolkits.jetbrains.services.codemodernizer.model.JobId import software.aws.toolkits.jetbrains.services.codemodernizer.model.ParseZipFailureReason import software.aws.toolkits.jetbrains.services.codemodernizer.model.ValidationResult +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.downloadClientInstructions import software.aws.toolkits.jetbrains.services.codemodernizer.utils.filterOnlyParentFiles import software.aws.toolkits.jetbrains.services.codemodernizer.utils.unzipFile import software.aws.toolkits.telemetry.CodeTransformPreValidationError +import java.io.ByteArrayOutputStream +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream import kotlin.io.path.Path import kotlin.io.path.createTempDirectory import kotlin.io.path.exists @@ -154,15 +161,43 @@ class CodeWhispererCodeModernizerTest : CodeWhispererCodeModernizerTestBase() { DownloadArtifactResult.ParseZipFailure( ParseZipFailureReason(TransformationDownloadArtifactType.LOGS, "Could not find build log") ) - val mockDownloadResult = listOf() - doReturn(mockDownloadResult).whenever(clientAdaptorSpy) + val mockZipBytes = ByteArrayOutputStream().use { bos -> + ZipOutputStream(bos).use { zos -> + zos.putNextEntry(ZipEntry("some-other-file.txt")) + zos.write("mock content".toByteArray()) + zos.closeEntry() + } + bos.toByteArray() + } + doReturn(listOf(mockZipBytes)).whenever(clientAdaptorSpy) .downloadExportResultArchive(jobId, null, TransformationDownloadArtifactType.LOGS) - doReturn(Pair(exampleZipPath, 0)).whenever(handler).unzipToPath(mockDownloadResult) val result = handler.downloadArtifact(jobId, TransformationDownloadArtifactType.LOGS, false) verify(clientAdaptorSpy, times(1)).downloadExportResultArchive(jobId, null, TransformationDownloadArtifactType.LOGS) assertEquals(expected, result) } + @Test + fun `downloadClientInstructions downloads and extracts patch file`() = runBlocking { + val jobId = JobId("test-job-id") + val artifactId = "test-artifact-id" + val mockZipBytes = ByteArrayOutputStream().use { bos -> + ZipOutputStream(bos).use { zos -> + zos.putNextEntry(ZipEntry("diff.patch")) + zos.write("mock content".toByteArray()) + zos.closeEntry() + } + bos.toByteArray() + } + doReturn(listOf(mockZipBytes)).whenever(clientAdaptorSpy).downloadExportResultArchive( + jobId, + artifactId, + TransformationDownloadArtifactType.CLIENT_INSTRUCTIONS + ) + val result = downloadClientInstructions(jobId, artifactId, project) + verify(clientAdaptorSpy).downloadExportResultArchive(jobId, artifactId, TransformationDownloadArtifactType.CLIENT_INSTRUCTIONS) + assertEquals(result.fileName.toString(), "diff.patch") + } + @Test fun `CodeModernizerArtifact can process a valid zip file`() { val artifact = CodeModernizerArtifact.create(exampleZipPath.toAbsolutePath().toString()) @@ -196,6 +231,30 @@ class CodeWhispererCodeModernizerTest : CodeWhispererCodeModernizerTestBase() { assert(tempDir.resolve(validZipPatchFilePath).exists()) } + @Test + fun `can unzip only sources folder`() { + val tempDir = createTempDirectory("test_zip") + val destDir = createTempDirectory("test_unzip") + // create test zip file with both source and non-source files + val zipPath = tempDir.resolve("test.zip") + ZipOutputStream(FileOutputStream(zipPath.toFile())).use { zos -> + zos.putNextEntry(ZipEntry("sources/Test.kt")) + zos.write("test content".toByteArray()) + zos.closeEntry() + + zos.putNextEntry(ZipEntry("other/Other.kt")) + zos.write("other content".toByteArray()) + zos.closeEntry() + } + + val result = unzipFile(zipPath, destDir, extractOnlySources = true) + assertTrue(result) + assertTrue(destDir.resolve("sources/Test.kt").exists()) + assertFalse(destDir.resolve("other/Other.kt").exists()) + tempDir.toFile().deleteRecursively() + destDir.toFile().deleteRecursively() + } + @Test fun `returns False when unable to unzip file`() { assertFalse(unzipFile(Path("dummy1"), Path("dummy2"))) diff --git a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerTestBase.kt b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerTestBase.kt index a02cd5baa97..9c20dfa9b5d 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerTestBase.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerTestBase.kt @@ -313,7 +313,6 @@ open class CodeWhispererCodeModernizerTestBase( ) testSessionSpy = spy(CodeModernizerSession(testSessionContextSpy, 0, 0)) - doNothing().whenever(testSessionSpy).deleteUploadArtifact(any()) doReturn(Job()).whenever(codeModernizerManagerSpy).launchModernizationJob(any(), any()) testCodeModernizerArtifact = spy( diff --git a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt index 948024cc99d..37482669432 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.jetbrains.services.codemodernizer + +import com.intellij.testFramework.LightVirtualFile import io.mockk.every import io.mockk.just import io.mockk.mockkStatic @@ -9,6 +11,7 @@ import io.mockk.runs import io.mockk.verify import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.yaml.YAMLFileType import org.junit.Before import org.junit.Test import org.mockito.Mockito @@ -17,20 +20,27 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException +import software.amazon.awssdk.services.codewhispererruntime.model.TransformationDownloadArtifact +import software.amazon.awssdk.services.codewhispererruntime.model.TransformationPlan import software.amazon.awssdk.services.codewhispererruntime.model.TransformationProgressUpdate import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStatus +import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStep import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformType +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.createClientSideBuildUploadZip import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getBillingText +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getClientInstructionArtifactId import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getTableMapping import software.aws.toolkits.jetbrains.services.codemodernizer.utils.parseBuildFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.pollTransformationStatusAndPlan import software.aws.toolkits.jetbrains.services.codemodernizer.utils.refreshToken +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.validateCustomVersionsFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.validateSctMetadata import software.aws.toolkits.jetbrains.utils.notifyStickyWarn import software.aws.toolkits.jetbrains.utils.rules.addFileToModule import software.aws.toolkits.resources.message import java.util.concurrent.atomic.AtomicBoolean +import java.util.zip.ZipFile import kotlin.io.path.createTempFile class CodeWhispererCodeModernizerUtilsTest : CodeWhispererCodeModernizerTestBase() { @@ -218,6 +228,83 @@ class CodeWhispererCodeModernizerUtilsTest : CodeWhispererCodeModernizerTestBase assertThat(expected).isEqualTo(actual) } + @Test + fun `getClientInstructionArtifactId extracts artifact ID from transformation plan`() { + val step1 = TransformationStep.builder() + .name("name of step 1") + .description("description of step 1") + .build() + val step2 = TransformationStep.builder() + .name("name of step 2") + .description("description of step 2") + .build() + val step3 = TransformationStep.builder() + .name("name of step 3") + .description("description of step 3") + .progressUpdates( + TransformationProgressUpdate.builder() + .name("Requesting client-side build") + .status("AWAITING_CLIENT_ACTION") + .downloadArtifacts( + TransformationDownloadArtifact.builder() + .downloadArtifactId("id-123") + .build() + ) + .build() + ) + .build() + val plan = TransformationPlan.builder() + .transformationSteps(listOf(step1, step2, step3)) + .build() + + val actual = getClientInstructionArtifactId(plan) + assertThat(actual).isEqualTo("id-123") + } + + @Test + fun `getClientInstructionArtifactId returns null when no download artifacts present`() { + val step1 = TransformationStep.builder() + .name("name of step 1") + .description("description of step 1") + .build() + val step2 = TransformationStep.builder() + .name("name of step 2") + .description("description of step 2") + .progressUpdates( + TransformationProgressUpdate.builder() + .name("NOT requesting client-side build") + .status("NOT awaiting_client_action") + .build() + ) + .build() + val plan = TransformationPlan.builder() + .transformationSteps(listOf(step1, step2)) + .build() + + val actual = getClientInstructionArtifactId(plan) + assertThat(actual).isNull() + } + + @Test + fun `createClientSideBuildUploadZip creates zip with manifest and build output`() { + val exitCode = 0 + val stdout = "Build completed successfully" + val zipFile = createClientSideBuildUploadZip(exitCode, stdout) + ZipFile(zipFile).use { zip -> + val manifestEntry = zip.getEntry("manifest.json") + assertThat(manifestEntry).isNotNull + val manifestContent = zip.getInputStream(manifestEntry).bufferedReader().use { it.readText() } + assertThat(manifestContent).contains("\"capability\":\"CLIENT_SIDE_BUILD\"") + assertThat(manifestContent).contains("\"exitCode\":0") + assertThat(manifestContent).contains("\"commandLogFileName\":\"build-output.log\"") + val logEntry = zip.getEntry("build-output.log") + assertThat(logEntry).isNotNull + val logContent = zip.getInputStream(logEntry).bufferedReader().use { it.readText() } + assertThat(logContent).isEqualTo("Build completed successfully") + } + zipFile.delete() + } + @Test fun `parseBuildFile can detect absolute paths in build file`() { val module = projectRule.module @@ -240,6 +327,69 @@ class CodeWhispererCodeModernizerUtilsTest : CodeWhispererCodeModernizerTestBase assertThat(expected).isEqualTo(actual) } + @Test + fun `WHEN validateCustomVersionsFile on fully valid yaml file THEN passes validation`() { + val sampleFileContents = """name: "custom-dependency-management" +description: "Custom dependency version management for Java migration from JDK 8/11/17 to JDK 17/21" +dependencyManagement: + dependencies: + - identifier: "com.example:library1" + targetVersion: "2.1.0" + versionProperty: "library1.version" + originType: "FIRST_PARTY" + plugins: + - identifier: "com.example.plugin" + targetVersion: "1.2.0" + versionProperty: "plugin.version" + """.trimIndent() + + val virtualFile = LightVirtualFile("test-valid.yaml", YAMLFileType.YML, sampleFileContents) + val isValidFile = validateCustomVersionsFile(virtualFile) + assertThat(isValidFile).isTrue() + } + + @Test + fun `WHEN validateCustomVersionsFile on invalid yaml file THEN fails validation`() { + val sampleFileContents = """name: "custom-dependency-management" +description: "Custom dependency version management for Java migration from JDK 8/11/17 to JDK 17/21" +invalidKey: + dependencies: + - identifier: "com.example:library1" + targetVersion: "2.1.0" + versionProperty: "library1.version" + originType: "FIRST_PARTY" + plugins: + - identifier: "com.example.plugin" + targetVersion: "1.2.0" + versionProperty: "plugin.version" + """.trimIndent() + + val virtualFile = LightVirtualFile("test-invalid.yaml", YAMLFileType.YML, sampleFileContents) + val isValidFile = validateCustomVersionsFile(virtualFile) + assertThat(isValidFile).isFalse() + } + + @Test + fun `WHEN validateCustomVersionsFile on non-yaml file THEN fails validation`() { + val sampleFileContents = """name: "custom-dependency-management" +description: "Custom dependency version management for Java migration from JDK 8/11/17 to JDK 17/21" +dependencyManagement: + dependencies: + - identifier: "com.example:library1" + targetVersion: "2.1.0" + versionProperty: "library1.version" + originType: "FIRST_PARTY" + plugins: + - identifier: "com.example.plugin" + targetVersion: "1.2.0" + versionProperty: "plugin.version" + """.trimIndent() + + val virtualFile = LightVirtualFile("test-invalid-file-type.txt", sampleFileContents) + val isValidFile = validateCustomVersionsFile(virtualFile) + assertThat(isValidFile).isFalse() + } + @Test fun `WHEN validateMetadataFile on fully valid sct file THEN passes validation`() { val sampleFileContents = """ diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTransformChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTransformChatConnector.ts index 86ac9a3fe6f..08d1335a03f 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTransformChatConnector.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/codeTransformChatConnector.ts @@ -267,8 +267,6 @@ export class CodeTransformChatConnector { tabType: 'codetransform', }) } else if (action.id === FormButtonIds.OpenMvnBuild) { - console.log('open_mvn_build') - this.sendMessageToExtension({ command: 'codetransform-open-mvn-build', tabID, @@ -280,7 +278,13 @@ export class CodeTransformChatConnector { tabID, tabType: 'codetransform', }) - } else if (action.id === FormButtonIds.CodeTransformInputSkipTests) { + } else if (action.id === FormButtonIds.CodeTransformInputContinue) { + this.sendMessageToExtension({ + command: 'codetransform-continue', + tabID, + tabType: 'codetransform', + }) + } else if (action.id === FormButtonIds.CodeTransformInputSkipTests) { this.sendMessageToExtension({ command: 'codetransform-confirm-skip-tests', tabID, @@ -294,6 +298,12 @@ export class CodeTransformChatConnector { tabType: 'codetransform', oneOrMultipleDiffsSelection: action.formItemValues?.oneOrMultipleDiffsSelection }) + } else if (action.id === FormButtonIds.CodeTransformInputCustomDependencyVersions) { + this.sendMessageToExtension({ + command: 'codetransform-input-confirm-custom-dependency-versions', + tabID, + tabType: 'codetransform', + }) } else if (action.id === FormButtonIds.OpenTransformationHub) { this.sendMessageToExtension({ command: 'codetransform-open-transform-hub', diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts index 5df19981b3f..01486f01f2a 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts @@ -36,8 +36,10 @@ type MessageCommand = | 'codetransform-select-sql-module-schema' | 'codetransform-cancel' | 'codetransform-stop' + | 'codetransform-continue' | 'codetransform-confirm-skip-tests' | 'codetransform-confirm-one-or-multiple-diffs' + | 'codetransform-input-confirm-custom-dependency-versions' | 'codetransform-new' | 'codetransform-open-transform-hub' | 'codetransform-open-mvn-build' diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/forms/constants.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/forms/constants.ts index e400e2a5541..22b913acf96 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/forms/constants.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/forms/constants.ts @@ -8,8 +8,10 @@ export const enum FormButtonIds { CodeTransformInputSQLMetadata = 'codetransform-input-select-sql-metadata', CodeTransformInputSQLModuleSchema = 'codetransform-input-select-sql-module-schema', CodeTransformInputCancel = 'codetransform-input-cancel', + CodeTransformInputContinue = 'codetransform-input-continue', CodeTransformInputSkipTests = 'codetransform-input-confirm-skip-tests', CodeTransformInputOneOrMultipleDiffs = 'codetransform-input-confirm-one-or-multiple-diffs', + CodeTransformInputCustomDependencyVersions = 'codetransform-input-confirm-custom-dependency-versions', OpenMvnBuild = 'open_mvn_build', StopTransform = 'stop_transform', OpenTransformationHub = 'open_transformation_hub', @@ -46,7 +48,9 @@ export const isFormButtonCodeTransform = (id: string): boolean => { id === FormButtonIds.CodeTransformInputSQLMetadata || id === FormButtonIds.CodeTransformInputSQLModuleSchema || id === FormButtonIds.CodeTransformInputSkipTests || + id === FormButtonIds.CodeTransformInputContinue || id === FormButtonIds.CodeTransformInputOneOrMultipleDiffs || + id === FormButtonIds.CodeTransformInputCustomDependencyVersions || id === FormButtonIds.CodeTransformViewDiff || id === FormButtonIds.CodeTransformViewSummary || id === FormButtonIds.CodeTransformViewBuildLog || diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index c7e667e7bc8..3720c69650e 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -647,6 +647,8 @@ codemodernizer.chat.message.download_failed_invalid_artifact=Sorry, I was unable codemodernizer.chat.message.download_failed_other=Sorry, I ran into an issue while trying to download your {0}. Please try again. {1} codemodernizer.chat.message.download_failed_ssl=Sorry, I couldn''t download your {0} because of an issue with your certificate. Please make sure all your certificates for your proxy client have been set up correctly for your IDE. codemodernizer.chat.message.download_failed_wildcard=Sorry, I couldn''t download your {0} because of an issue with your proxy client. Please check your IDE proxy settings and remove any wildcard (*) references, then restart your IDE. +codemodernizer.chat.message.enter_jdk_name=Enter the name of the {0} you are using. You can find the name in File > Project Structure > Platform Settings > SDKs. If you do not see the name of your JDK in the SDK settings, add your JDK, and then return to this chat and enter the name here. +codemodernizer.chat.message.enter_jdk_name_error=I could not find "{0}" in File > Project Structure > Platform Settings > SDKs. Please add the target JDK there and try again. codemodernizer.chat.message.error_request=Request failed codemodernizer.chat.message.follow_up.new_transformation=Start a new transformation codemodernizer.chat.message.hil.cannot_resume=I ran into an issue trying to resume your transformation. @@ -684,7 +686,7 @@ codemodernizer.chat.message.result.success=I successfully completed your transfo codemodernizer.chat.message.result.success.multiple_diffs=I successfully completed your transformation. You will be able to accept changes from one diff at a time. If you reject changes in one diff, you will not be able to view or accept changes in the other diffs. The transformation summary has details about the changes I'm proposing. codemodernizer.chat.message.result.zip_too_large=Sorry, your project size exceeds the Amazon Q Code Transformation upload limit of 2GB. codemodernizer.chat.message.resume_ongoing=I'm still transforming your code. It can take 10 to 30 minutes to upgrade your code, depending on the size of your module. To monitor progress, go to the Transformation Hub. -codemodernizer.chat.message.skip_tests=I will build your module using `mvn test` by default. If you would like me to build your module without running unit tests, I will use `mvn test-compile`. +codemodernizer.chat.message.skip_tests=I will build your project using `mvn clean test` by default. If you would like me to build your project without running unit tests, I will use `mvn clean test-compile`. codemodernizer.chat.message.skip_tests_form.response=Okay, I will {0} when building your module. codemodernizer.chat.message.skip_tests_form.run_tests=Run unit tests codemodernizer.chat.message.skip_tests_form.skip=Skip unit tests @@ -752,9 +754,9 @@ codemodernizer.migration_plan.header.awsq=
{0} lines of code were submitted for transformation. If you reach the quota for lines of code included in your subscription, you will be charged ${1} for each additional line of code. You might be charged up to ${2} for this transformation. To avoid being charged, stop the transformation job before it completes. For more information on pricing and quotas, see Amazon Q Developer pricing.

codemodernizer.migration_plan.header.description=Plan to transform your module codemodernizer.migration_plan.header.title=Code Transformation plan by Amazon Q +codemodernizer.migration_plan.substeps.description_completed=Build completed codemodernizer.migration_plan.substeps.description_failed=Build failed codemodernizer.migration_plan.substeps.description_stopped=Job is stopped -codemodernizer.migration_plan.substeps.description_succeed=Build succeeded codemodernizer.migration_summary.header.title=Transformation summary codemodernizer.notification.info.download.started.content=Downloading the updated code codemodernizer.notification.info.download.started.title=Download Started diff --git a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/transformTests/TransformChatTest.kt b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/transformTests/TransformChatTest.kt index c51be54d50f..82951c22738 100644 --- a/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/transformTests/TransformChatTest.kt +++ b/ui-tests-starter/tst-243+/software/aws/toolkits/jetbrains/uitests/transformTests/TransformChatTest.kt @@ -81,7 +81,25 @@ async function testNavigation() { const button = document.querySelector('button[action-id="codetransform-input-confirm-one-or-multiple-diffs"]') button.click() }) - +/* + const selectCustomVersionsForm = await page.waitForSelector('button[action-id="codetransform-input-confirm-custom-dependency-versions"]', { + timeout: 5000 + }) + console.log('Custom dependency versions file form appeared:', selectCustomVersionsForm !== null) + + await page.evaluate(() => { + const button = document.querySelector('button[action-id="codetransform-input-continue"]') + button.click() + }) + + await page.type('.mynah-chat-prompt-input', 'dummy-target-jdk-name-here') + await page.keyboard.press('Enter') + + const errorMessage = await page.waitForSelector('text/I could not find "dummy-target-jdk-name-here" in File > Project Structure > Platform Settings > SDKs.', { + timeout: 5000 + }) +*/ + // TODO: delete errorMessage below, and uncomment the above when releasing CSB const errorMessage = await page.waitForSelector('text/Sorry, I couldn\'t run the Maven clean install command', { timeout: 5000 }) @@ -149,6 +167,9 @@ class TransformChatTest { "Skip tests form appeared: true", "One or multiple diffs form appeared: true", "couldn't run the Maven clean install command" + // TODO: delete line above, and uncomment lines below when releasing CSB + // "I could not find \"dummy-target-jdk-name-here\"", + // "Custom dependency versions file form appeared: true", ) } }