Skip to content

feat(amazonq): enable client-side build / run JAR #7226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Feature",
"description": "/transform: run all builds client-side"
}
7 changes: 2 additions & 5 deletions packages/amazonq/test/e2e/amazonq/transformByQ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,6 @@ describe('Amazon Q Code Transformation', function () {
waitIntervalInMs: 1000,
})

// TO-DO: add this back when releasing CSB
/*
const customDependencyVersionPrompt = tab.getChatItems().pop()
assert.strictEqual(
customDependencyVersionPrompt?.body?.includes('You can optionally upload a YAML file'),
Expand All @@ -143,15 +141,14 @@ describe('Amazon Q Code Transformation', function () {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})
*/

const sourceJdkPathPrompt = tab.getChatItems().pop()
assert.strictEqual(sourceJdkPathPrompt?.body?.includes('Enter the path to JDK 8'), true)

tab.addChatMessage({ prompt: '/dummy/path/to/jdk8' })

// 2 additional chat messages get sent after JDK path submitted; wait for both of them
await tab.waitForEvent(() => tab.getChatItems().length > 10, {
await tab.waitForEvent(() => tab.getChatItems().length > 15, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})
Expand All @@ -173,7 +170,7 @@ describe('Amazon Q Code Transformation', function () {
text: 'View summary',
})

await tab.waitForEvent(() => tab.getChatItems().length > 11, {
await tab.waitForEvent(() => tab.getChatItems().length > 18, {
waitTimeoutInMs: 5000,
waitIntervalInMs: 1000,
})
Expand Down
Binary file not shown.
30 changes: 5 additions & 25 deletions packages/core/src/amazonqGumby/chat/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,9 @@ import {
} from '../../errors'
import * as CodeWhispererConstants from '../../../codewhisperer/models/constants'
import MessengerUtils, { ButtonActions, GumbyCommands } from './messenger/messengerUtils'
import { CancelActionPositions, JDKToTelemetryValue, telemetryUndefined } from '../../telemetry/codeTransformTelemetry'
import { CancelActionPositions } from '../../telemetry/codeTransformTelemetry'
import { openUrl } from '../../../shared/utilities/vsCodeUtils'
import {
telemetry,
CodeTransformJavaTargetVersionsAllowed,
CodeTransformJavaSourceVersionsAllowed,
} from '../../../shared/telemetry/telemetry'
import { telemetry } from '../../../shared/telemetry/telemetry'
import { CodeTransformTelemetryState } from '../../telemetry/codeTransformTelemetryState'
import DependencyVersions from '../../models/dependencies'
import { getStringHash } from '../../../shared/utilities/textUtilities'
Expand Down Expand Up @@ -308,7 +304,6 @@ export class GumbyController {
}

private async validateLanguageUpgradeProjects(message: any) {
let telemetryJavaVersion = JDKToTelemetryValue(JDKVersion.UNSUPPORTED) as CodeTransformJavaSourceVersionsAllowed
try {
const validProjects = await telemetry.codeTransform_validateProject.run(async () => {
telemetry.record({
Expand All @@ -317,12 +312,6 @@ export class GumbyController {
})

const validProjects = await getValidLanguageUpgradeCandidateProjects()
if (validProjects.length > 0) {
// validProjects[0].JDKVersion will be undefined if javap errors out or no .class files found, so call it UNSUPPORTED
const javaVersion = validProjects[0].JDKVersion ?? JDKVersion.UNSUPPORTED
telemetryJavaVersion = JDKToTelemetryValue(javaVersion) as CodeTransformJavaSourceVersionsAllowed
}
telemetry.record({ codeTransformLocalJavaVersion: telemetryJavaVersion })
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of useless telemetry, so getting rid of some things and will remove them from the commons package shortly

return validProjects
})
return validProjects
Expand Down Expand Up @@ -437,9 +426,7 @@ export class GumbyController {
userChoice: skipTestsSelection,
})
this.messenger.sendSkipTestsSelectionMessage(skipTestsSelection, message.tabID)
this.promptJavaHome('source', message.tabID)
// TO-DO: delete line above and uncomment line below when releasing CSB
// await this.messenger.sendCustomDependencyVersionMessage(message.tabID)
await this.messenger.sendCustomDependencyVersionMessage(message.tabID)
})
}

Expand All @@ -465,16 +452,9 @@ export class GumbyController {
const fromJDKVersion: JDKVersion = message.formSelectedValues['GumbyTransformJdkFromForm']

telemetry.record({
// TODO: remove JavaSource/TargetVersionsAllowed when BI is updated to use source/target
codeTransformJavaSourceVersionsAllowed: JDKToTelemetryValue(
fromJDKVersion
) as CodeTransformJavaSourceVersionsAllowed,
codeTransformJavaTargetVersionsAllowed: JDKToTelemetryValue(
toJDKVersion
) as CodeTransformJavaTargetVersionsAllowed,
source: fromJDKVersion,
target: toJDKVersion,
codeTransformProjectId: pathToProject === undefined ? telemetryUndefined : getStringHash(pathToProject),
codeTransformProjectId: pathToProject === undefined ? undefined : getStringHash(pathToProject),
userChoice: 'Confirm-Java',
})

Expand Down Expand Up @@ -503,7 +483,7 @@ export class GumbyController {
const schema: string = message.formSelectedValues['GumbyTransformSQLSchemaForm']

telemetry.record({
codeTransformProjectId: pathToProject === undefined ? telemetryUndefined : getStringHash(pathToProject),
codeTransformProjectId: pathToProject === undefined ? undefined : getStringHash(pathToProject),
source: transformByQState.getSourceDB(),
target: transformByQState.getTargetDB(),
userChoice: 'Confirm-SQL',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ export class Messenger {
message = CodeWhispererConstants.noPomXmlFoundChatMessage
break
case 'could-not-compile-project':
message = CodeWhispererConstants.cleanInstallErrorChatMessage
message = CodeWhispererConstants.cleanTestCompileErrorChatMessage
break
case 'invalid-java-home':
message = CodeWhispererConstants.noJavaHomeFoundChatMessage
Expand Down Expand Up @@ -731,7 +731,7 @@ ${codeSnippet}
tabID
)
)
const sampleYAML = `name: "custom-dependency-management"
const sampleYAML = `name: "dependency-upgrade"
description: "Custom dependency version management for Java migration from JDK 8/11/17 to JDK 17/21"

dependencyManagement:
Expand All @@ -744,7 +744,7 @@ dependencyManagement:
targetVersion: "3.0.0"
originType: "THIRD_PARTY"
plugins:
- identifier: "com.example.plugin"
- identifier: "com.example:plugin"
targetVersion: "1.2.0"
versionProperty: "plugin.version" # Optional`

Expand Down
6 changes: 0 additions & 6 deletions packages/core/src/amazonqGumby/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,6 @@ export class NoMavenJavaProjectsFoundError extends ToolkitError {
}
}

export class ZipExceedsSizeLimitError extends ToolkitError {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We often allowlist certain customers for ZIP size limits greater than the default of 2GB, so remove this client-side check (which didn't seem to work well anyway) and let our service fail the transformation with a relevant error message if the ZIP is too large.

constructor() {
super('Zip file exceeds size limit', { code: 'ZipFileExceedsSizeLimit' })
}
}

export class AlternateDependencyVersionsNotFoundError extends Error {
constructor() {
super('No available versions for dependency update')
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/codewhisperer/client/codewhisperer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class DefaultCodeWhispererClient {
/**
* @description Use this function to get the status of the code transformation. We should
* be polling this function periodically to get updated results. When this function
* returns COMPLETED we know the transformation is done.
* returns PARTIALLY_COMPLETED or COMPLETED we know the transformation is done.
*/
public async codeModernizerGetCodeTransformation(
request: CodeWhispererUserClient.GetTransformationRequest
Expand All @@ -272,15 +272,15 @@ export class DefaultCodeWhispererClient {
}

/**
* @description After the job has been PAUSED we need to get user intervention. Once that user
* intervention has been handled we can resume the transformation job.
* @description During client-side build, or after the job has been PAUSED we need to get user intervention.
* Once that user action has been handled we can resume the transformation job.
* @params transformationJobId - String id returned from StartCodeTransformationResponse
* @params userActionStatus - String to determine what action the user took, if any.
*/
public async codeModernizerResumeTransformation(
request: CodeWhispererUserClient.ResumeTransformationRequest
): Promise<PromiseResult<CodeWhispererUserClient.ResumeTransformationResponse, AWSError>> {
return (await this.createUserSdkClient()).resumeTransformation(request).promise()
return (await this.createUserSdkClient(8)).resumeTransformation(request).promise()
}

/**
Expand Down
75 changes: 30 additions & 45 deletions packages/core/src/codewhisperer/commands/startTransformByQ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as vscode from 'vscode'
import * as fs from 'fs' // eslint-disable-line no-restricted-imports
import os from 'os'
import path from 'path'
import { getLogger } from '../../shared/logger/logger'
import * as CodeWhispererConstants from '../models/constants'
Expand All @@ -16,7 +17,6 @@ import {
jobPlanProgress,
FolderInfo,
ZipManifest,
TransformByQStatus,
TransformationType,
TransformationCandidateProject,
RegionProfile,
Expand All @@ -43,7 +43,6 @@ import {
validateOpenProjects,
} from '../service/transformByQ/transformProjectValidationHandler'
import {
getVersionData,
prepareProjectDependencies,
runMavenDependencyUpdateCommands,
} from '../service/transformByQ/transformMavenHandler'
Expand Down Expand Up @@ -82,7 +81,7 @@ import { AuthUtil } from '../util/authUtil'

export function getFeedbackCommentData() {
const jobId = transformByQState.getJobId()
const s = `Q CodeTransform jobId: ${jobId ? jobId : 'none'}`
const s = `Q CodeTransformation jobId: ${jobId ? jobId : 'none'}`
return s
}

Expand Down Expand Up @@ -110,10 +109,10 @@ export async function processSQLConversionTransformFormInput(pathToProject: stri

export async function compileProject() {
try {
const dependenciesFolder: FolderInfo = getDependenciesFolderInfo()
const dependenciesFolder: FolderInfo = await getDependenciesFolderInfo()
transformByQState.setDependencyFolderInfo(dependenciesFolder)
const modulePath = transformByQState.getProjectPath()
await prepareProjectDependencies(dependenciesFolder, modulePath)
const projectPath = transformByQState.getProjectPath()
await prepareProjectDependencies(dependenciesFolder.path, projectPath)
} catch (err) {
// open build-logs.txt file to show user error logs
await writeAndShowBuildLogs(true)
Expand Down Expand Up @@ -175,18 +174,15 @@ export async function humanInTheLoopRetryLogic(jobId: string, profile: RegionPro
if (status === 'PAUSED') {
const hilStatusFailure = await initiateHumanInTheLoopPrompt(jobId)
if (hilStatusFailure) {
// We rejected the changes and resumed the job and should
// try to resume normal polling asynchronously
// resume polling
void humanInTheLoopRetryLogic(jobId, profile)
}
} else {
await finalizeTransformByQ(status)
}
} catch (error) {
status = 'FAILED'
// TODO if we encounter error in HIL, do we stop job?
await finalizeTransformByQ(status)
// bubble up error to callee function
Copy link
Contributor Author

@dhasani23 dhasani23 May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

caller*

Our HIL (Human-in-the-Loop) feature I believe is effectively obsolete once this PR is merged; will discuss to confirm and then hopefully will be able to delete all of the dead HIL-related code here.

throw error
}
}
Expand Down Expand Up @@ -225,11 +221,9 @@ export async function preTransformationUploadCode() {

const payloadFilePath = zipCodeResult.tempFilePath
const zipSize = zipCodeResult.fileSize
const dependenciesCopied = zipCodeResult.dependenciesCopied

telemetry.record({
codeTransformTotalByteSize: zipSize,
codeTransformDependenciesCopied: dependenciesCopied,
})

transformByQState.setPayloadFilePath(payloadFilePath)
Expand Down Expand Up @@ -408,7 +402,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) {

// 7) We need to take that output of maven and use CreateUploadUrl
const uploadFolderInfo = humanInTheLoopManager.getUploadFolderInfo()
await prepareProjectDependencies(uploadFolderInfo, uploadFolderInfo.path)
await prepareProjectDependencies(uploadFolderInfo.path, uploadFolderInfo.path)
// zipCode side effects deletes the uploadFolderInfo right away
const uploadResult = await zipCode({
dependenciesFolder: uploadFolderInfo,
Expand Down Expand Up @@ -449,13 +443,11 @@ export async function finishHumanInTheLoop(selectedDependency?: string) {
await terminateHILEarly(jobId)
void humanInTheLoopRetryLogic(jobId, profile)
} finally {
// Always delete the dependency directories
telemetry.codeTransform_humanInTheLoop.emit({
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
codeTransformJobId: jobId,
codeTransformMetadata: CodeTransformTelemetryState.instance.getCodeTransformMetaDataString(),
result: hilResult,
// TODO: make a generic reason field for telemetry logging so we don't log sensitive PII data
reason: hilResult === MetadataResult.Fail ? 'Runtime error occurred' : undefined,
})
await HumanInTheLoopManager.instance.cleanUpArtifacts()
Expand Down Expand Up @@ -504,7 +496,7 @@ export async function startTransformationJob(
throw new JobStartError()
}

await sleep(2000) // sleep before polling job to prevent ThrottlingException
await sleep(5000) // sleep before polling job status to prevent ThrottlingException
throwIfCancelled()

return jobId
Expand All @@ -523,9 +515,7 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string, prof
transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToCompleteJobChatMessage)
}

// Since we don't yet have a good way of knowing what the error was,
// we try to fetch any build failure artifacts that may exist so that we can optionally
// show them to the user if they exist.
// try to download pre-build error logs if available
let pathToLog = ''
try {
const tempToolkitFolder = await makeTemporaryToolkitFolder()
Expand Down Expand Up @@ -689,23 +679,16 @@ export async function postTransformationJob() {
const durationInMs = calculateTotalLatency(CodeTransformTelemetryState.instance.getStartTime())
const resultStatusMessage = transformByQState.getStatus()

if (transformByQState.getTransformationType() !== TransformationType.SQL_CONVERSION) {
// the below is only applicable when user is doing a Java 8/11 language upgrade
const versionInfo = await getVersionData()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was executing a shell command at the end of every transformation just to emit telemetry on the user's Maven version, which nobody even looked at.

const mavenVersionInfoMessage = `${versionInfo[0]} (${transformByQState.getMavenName()})`
const javaVersionInfoMessage = `${versionInfo[1]} (${transformByQState.getMavenName()})`

telemetry.codeTransform_totalRunTime.emit({
buildSystemVersion: mavenVersionInfoMessage,
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
codeTransformJobId: transformByQState.getJobId(),
codeTransformResultStatusMessage: resultStatusMessage,
codeTransformRunTimeLatency: durationInMs,
codeTransformLocalJavaVersion: javaVersionInfoMessage,
result: resultStatusMessage === TransformByQStatus.Succeeded ? MetadataResult.Pass : MetadataResult.Fail,
reason: `${resultStatusMessage}-${chatMessage}`,
})
}
telemetry.codeTransform_totalRunTime.emit({
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
codeTransformJobId: transformByQState.getJobId(),
codeTransformResultStatusMessage: resultStatusMessage,
codeTransformRunTimeLatency: durationInMs,
result:
transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded()
? MetadataResult.Pass
: MetadataResult.Fail,
})

if (transformByQState.isSucceeded()) {
void vscode.window.showInformationMessage(CodeWhispererConstants.jobCompletedNotification, {
Expand All @@ -728,9 +711,14 @@ export async function postTransformationJob() {
})
}

if (transformByQState.getPayloadFilePath() !== '') {
if (transformByQState.getPayloadFilePath()) {
// delete original upload ZIP at very end of transformation
fs.rmSync(transformByQState.getPayloadFilePath(), { recursive: true, force: true })
fs.rmSync(transformByQState.getPayloadFilePath(), { force: true })
}
// delete temporary build logs file
const logFilePath = path.join(os.tmpdir(), 'build-logs.txt')
if (fs.existsSync(logFilePath)) {
fs.rmSync(logFilePath, { force: true })
}

// attempt download for user
Expand All @@ -746,14 +734,11 @@ export async function transformationJobErrorHandler(error: any) {
transformByQState.setToFailed()
transformByQState.setPolledJobStatus('FAILED')
// jobFailureErrorNotification should always be defined here
let displayedErrorMessage =
const displayedErrorMessage =
transformByQState.getJobFailureErrorNotification() ?? CodeWhispererConstants.failedToCompleteJobNotification
if (transformByQState.getJobFailureMetadata() !== '') {
displayedErrorMessage += ` ${transformByQState.getJobFailureMetadata()}`
transformByQState.setJobFailureErrorChatMessage(
`${transformByQState.getJobFailureErrorChatMessage()} ${transformByQState.getJobFailureMetadata()}`
)
}
transformByQState.setJobFailureErrorChatMessage(
transformByQState.getJobFailureErrorChatMessage() ?? CodeWhispererConstants.failedToCompleteJobChatMessage
)
void vscode.window
.showErrorMessage(displayedErrorMessage, CodeWhispererConstants.amazonQFeedbackText)
.then((choice) => {
Expand Down
Loading
Loading