Skip to content

Commit 0651afd

Browse files
authored
feat(amazonq): client-side build support aws#6771
## Problem We should perform builds client-side (rather than server-side) to handle customers’ unique / challenging local environments, which often cause our server-side build to fail. ## Solution Periodically download intermediate `diff.patch` files, apply them to a copy of the project, perform the local build, then upload the build logs and resume the transformation.
1 parent c2f1210 commit 0651afd

File tree

16 files changed

+635
-247
lines changed

16 files changed

+635
-247
lines changed

packages/amazonq/test/e2e/amazonq/transformByQ.test.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ describe('Amazon Q Code Transformation', function () {
7575
},
7676
])
7777

78+
transformByQState.setSourceJDKVersion(JDKVersion.JDK8)
79+
transformByQState.setTargetJDKVersion(JDKVersion.JDK17)
80+
7881
tab.addChatMessage({ command: '/transform' })
7982

8083
// wait for /transform to respond with some intro messages and the first user input form
@@ -141,16 +144,34 @@ describe('Amazon Q Code Transformation', function () {
141144
formItemValues: oneOrMultipleDiffsFormValues,
142145
})
143146

144-
// 2 additional chat messages (including message with 4th form) get sent after 3rd form submitted; wait for both of them
147+
// 2 additional chat messages get sent after 3rd form submitted; wait for both of them
145148
await tab.waitForEvent(() => tab.getChatItems().length > 11, {
146149
waitTimeoutInMs: 5000,
147150
waitIntervalInMs: 1000,
148151
})
149-
const jdkPathPrompt = tab.getChatItems().pop()
150-
assert.strictEqual(jdkPathPrompt?.body?.includes('Enter the path to JDK'), true)
151152

152-
// 2 additional chat messages get sent after 4th form submitted; wait for both of them
153+
// TO-DO: add this back when releasing CSB
154+
/*
155+
const customDependencyVersionPrompt = tab.getChatItems().pop()
156+
assert.strictEqual(
157+
customDependencyVersionPrompt?.body?.includes('You can optionally upload a YAML file'),
158+
true
159+
)
160+
tab.clickCustomFormButton({ id: 'gumbyTransformFormContinue' })
161+
162+
// 2 additional chat messages get sent after Continue button clicked; wait for both of them
163+
await tab.waitForEvent(() => tab.getChatItems().length > 13, {
164+
waitTimeoutInMs: 5000,
165+
waitIntervalInMs: 1000,
166+
})
167+
*/
168+
169+
const sourceJdkPathPrompt = tab.getChatItems().pop()
170+
assert.strictEqual(sourceJdkPathPrompt?.body?.includes('Enter the path to JDK 8'), true)
171+
153172
tab.addChatMessage({ prompt: '/dummy/path/to/jdk8' })
173+
174+
// 2 additional chat messages get sent after JDK path submitted; wait for both of them
154175
await tab.waitForEvent(() => tab.getChatItems().length > 13, {
155176
waitTimeoutInMs: 5000,
156177
waitIntervalInMs: 1000,
@@ -401,7 +422,7 @@ describe('Amazon Q Code Transformation', function () {
401422

402423
it('WHEN transforming a Java 8 project E2E THEN job is successful', async function () {
403424
transformByQState.setTransformationType(TransformationType.LANGUAGE_UPGRADE)
404-
await setMaven()
425+
setMaven()
405426
await startTransformByQ.processLanguageUpgradeTransformFormInput(tempDir, JDKVersion.JDK8, JDKVersion.JDK17)
406427
await startTransformByQ.startTransformByQ()
407428
assert.strictEqual(transformByQState.getPolledJobStatus(), 'COMPLETED')

packages/amazonq/test/unit/amazonqGumby/transformApiHandler.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import assert from 'assert'
66
import {
77
TransformationProgressUpdate,
88
TransformationStep,
9+
findDownloadArtifactProgressUpdate,
910
findDownloadArtifactStep,
1011
getArtifactsFromProgressUpdate,
1112
} from 'aws-core-vscode/codewhisperer/node'
@@ -95,4 +96,55 @@ describe('Amazon Q Transform - transformApiHandler tests', function () {
9596
assert.strictEqual(progressUpdate, undefined)
9697
})
9798
})
99+
100+
describe('findDownloadArtifactProgressUpdate', function () {
101+
it('will return correct progress update from transformationStep', function () {
102+
const transformationStepsFixture: TransformationStep[] = [
103+
{
104+
id: 'dummy-id',
105+
name: 'Step name',
106+
description: 'Step description',
107+
status: 'TRANSFORMING',
108+
progressUpdates: [
109+
{
110+
name: 'Progress update name',
111+
status: 'AWAITING_CLIENT_ACTION',
112+
description: 'Client-side build happening now',
113+
startTime: new Date(),
114+
endTime: new Date(),
115+
downloadArtifacts: [
116+
{
117+
downloadArtifactId: 'some-download-artifact-id',
118+
downloadArtifactType: 'some-download-artifact-type',
119+
},
120+
],
121+
},
122+
],
123+
startTime: new Date(),
124+
endTime: new Date(),
125+
},
126+
]
127+
const progressUpdate = findDownloadArtifactProgressUpdate(transformationStepsFixture)
128+
assert.strictEqual(progressUpdate, transformationStepsFixture[0].progressUpdates?.[0])
129+
})
130+
131+
it('will return undefined if step status is NOT AWAITING_CLIENT_ACTION', function () {
132+
const transformationStepsFixture: TransformationStep[] = [
133+
{
134+
id: 'random-id',
135+
name: 'not-awaiting-client-action step name',
136+
description: 'not-awaiting-client-action step description',
137+
status: 'TRANSFORMING',
138+
progressUpdates: [
139+
{
140+
name: 'some progress update name',
141+
status: 'SOMETHING-BESIDES-AWAITING_CLIENT_ACTION',
142+
},
143+
],
144+
},
145+
]
146+
const progressUpdate = findDownloadArtifactProgressUpdate(transformationStepsFixture)
147+
assert.strictEqual(progressUpdate, undefined)
148+
})
149+
})
98150
})

packages/core/src/amazonqGumby/chat/controller/controller.ts

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,13 @@ import {
2525
processSQLConversionTransformFormInput,
2626
startTransformByQ,
2727
stopTransformByQ,
28-
validateCanCompileProject,
2928
getValidSQLConversionCandidateProjects,
3029
openHilPomFile,
3130
} from '../../../codewhisperer/commands/startTransformByQ'
3231
import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../codewhisperer/models/model'
3332
import {
3433
AbsolutePathDetectedError,
3534
AlternateDependencyVersionsNotFoundError,
36-
JavaHomeNotSetError,
3735
JobStartError,
3836
ModuleUploadError,
3937
NoJavaProjectsFoundError,
@@ -59,8 +57,10 @@ import {
5957
openBuildLogFile,
6058
parseBuildFile,
6159
validateSQLMetadataFile,
60+
validateCustomVersionsFile,
6261
} from '../../../codewhisperer/service/transformByQ/transformFileHandler'
6362
import { getAuthType } from '../../../auth/utils'
63+
import fs from '../../../shared/fs/fs'
6464

6565
// These events can be interactions within the chat,
6666
// or elsewhere in the IDE
@@ -243,7 +243,7 @@ export class GumbyController {
243243
CodeTransformTelemetryState.instance.setSessionId()
244244

245245
this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_TRANSFORMATION_OBJECTIVE
246-
this.messenger.sendStaticTextResponse('choose-transformation-objective', message.tabID)
246+
this.messenger.sendMessage(CodeWhispererConstants.chooseTransformationObjective, message.tabID, 'ai-prompt')
247247
this.messenger.sendChatInputEnabled(message.tabID, true)
248248
this.messenger.sendUpdatePlaceholder(
249249
message.tabID,
@@ -299,7 +299,7 @@ export class GumbyController {
299299
const validProjects = await this.validateSQLConversionProjects(message)
300300
if (validProjects.length > 0) {
301301
this.sessionStorage.getSession().updateCandidateProjects(validProjects)
302-
await this.messenger.sendSelectSQLMetadataFileMessage(message.tabID)
302+
this.messenger.sendSelectSQLMetadataFileMessage(message.tabID)
303303
}
304304
})
305305
.catch((err) => {
@@ -383,6 +383,18 @@ export class GumbyController {
383383
case ButtonActions.SELECT_SQL_CONVERSION_METADATA_FILE:
384384
await this.processMetadataFile(message)
385385
break
386+
case ButtonActions.SELECT_CUSTOM_DEPENDENCY_VERSION_FILE:
387+
await this.processCustomDependencyVersionFile(message)
388+
break
389+
case ButtonActions.CONTINUE_TRANSFORMATION_FORM:
390+
this.messenger.sendMessage(
391+
CodeWhispererConstants.continueWithoutYamlMessage,
392+
message.tabID,
393+
'ai-prompt'
394+
)
395+
transformByQState.setCustomDependencyVersionFilePath('')
396+
this.promptJavaHome('source', message.tabID)
397+
break
386398
case ButtonActions.VIEW_TRANSFORMATION_HUB:
387399
await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB, CancelActionPositions.Chat)
388400
break
@@ -403,7 +415,7 @@ export class GumbyController {
403415
await this.continueJobWithSelectedDependency(message)
404416
break
405417
case ButtonActions.CANCEL_DEPENDENCY_FORM:
406-
this.messenger.sendUserPrompt('Cancel', message.tabID)
418+
this.messenger.sendMessage('Cancel', message.tabID, 'prompt')
407419
await this.continueTransformationWithoutHIL(message)
408420
break
409421
case ButtonActions.OPEN_FILE:
@@ -448,11 +460,27 @@ export class GumbyController {
448460
})
449461

450462
this.messenger.sendOneOrMultipleDiffsMessage(oneOrMultipleDiffsSelection, message.tabID)
451-
// perform local build
452-
await this.validateBuildWithPromptOnError(message)
463+
this.promptJavaHome('source', message.tabID)
464+
// TO-DO: delete line above and uncomment line below when releasing CSB
465+
// await this.messenger.sendCustomDependencyVersionMessage(message.tabID)
453466
})
454467
}
455468

469+
private promptJavaHome(type: 'source' | 'target', tabID: any) {
470+
let jdkVersion = undefined
471+
if (type === 'source') {
472+
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_SOURCE_JAVA_HOME
473+
jdkVersion = transformByQState.getSourceJDKVersion()
474+
} else if (type === 'target') {
475+
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_TARGET_JAVA_HOME
476+
jdkVersion = transformByQState.getTargetJDKVersion()
477+
}
478+
const message = MessengerUtils.createJavaHomePrompt(jdkVersion)
479+
this.messenger.sendMessage(message, tabID, 'ai-prompt')
480+
this.messenger.sendChatInputEnabled(tabID, true)
481+
this.messenger.sendUpdatePlaceholder(tabID, CodeWhispererConstants.enterJavaHomePlaceholder)
482+
}
483+
456484
private async handleUserLanguageUpgradeProjectChoice(message: any) {
457485
await telemetry.codeTransform_submitSelection.run(async () => {
458486
const pathToProject: string = message.formSelectedValues['GumbyTransformLanguageUpgradeProjectForm']
@@ -521,66 +549,59 @@ export class GumbyController {
521549
})
522550
}
523551

524-
private async prepareLanguageUpgradeProject(message: { pathToJavaHome: string; tabID: string }) {
525-
if (message.pathToJavaHome) {
526-
transformByQState.setJavaHome(message.pathToJavaHome)
527-
getLogger().info(
528-
`CodeTransformation: using JAVA_HOME = ${transformByQState.getJavaHome()} since source JDK does not match Maven JDK`
529-
)
530-
}
531-
532-
// Pre-build project locally
552+
private async prepareLanguageUpgradeProject(tabID: string) {
553+
// build project locally
533554
try {
534555
this.sessionStorage.getSession().conversationState = ConversationState.COMPILING
535-
this.messenger.sendCompilationInProgress(message.tabID)
556+
this.messenger.sendCompilationInProgress(tabID)
536557
await compileProject()
537558
} catch (err: any) {
538-
this.messenger.sendUnrecoverableErrorResponse('could-not-compile-project', message.tabID)
559+
this.messenger.sendUnrecoverableErrorResponse('could-not-compile-project', tabID)
539560
// reset state to allow "Start a new transformation" button to work
540561
this.sessionStorage.getSession().conversationState = ConversationState.IDLE
541562
throw err
542563
}
543564

544-
this.messenger.sendCompilationFinished(message.tabID)
565+
this.messenger.sendCompilationFinished(tabID)
545566

546567
// since compilation can potentially take a long time, double check auth
547568
const authState = await AuthUtil.instance.getChatAuthState()
548569
if (authState.amazonQ !== 'connected') {
549-
void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID)
570+
void this.messenger.sendAuthNeededExceptionMessage(authState, tabID)
550571
this.sessionStorage.getSession().isAuthenticating = true
551572
return
552573
}
553574

554575
// give user a non-blocking warning if build file appears to contain absolute paths
555576
await parseBuildFile()
556577

557-
this.messenger.sendAsyncEventProgress(
558-
message.tabID,
559-
true,
560-
undefined,
561-
GumbyNamedMessages.JOB_SUBMISSION_STATUS_MESSAGE
562-
)
563-
this.messenger.sendJobSubmittedMessage(message.tabID)
578+
this.messenger.sendAsyncEventProgress(tabID, true, undefined, GumbyNamedMessages.JOB_SUBMISSION_STATUS_MESSAGE)
579+
this.messenger.sendJobSubmittedMessage(tabID)
564580
this.sessionStorage.getSession().conversationState = ConversationState.JOB_SUBMITTED
565581
await startTransformByQ()
566582
}
567583

568-
// only for Language Upgrades
569-
private async validateBuildWithPromptOnError(message: any | undefined = undefined): Promise<void> {
570-
try {
571-
// Check Java Home is set (not yet prebuilding)
572-
await validateCanCompileProject()
573-
} catch (err: any) {
574-
if (err instanceof JavaHomeNotSetError) {
575-
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_JAVA_HOME
576-
this.messenger.sendStaticTextResponse('java-home-not-set', message.tabID)
577-
this.messenger.sendChatInputEnabled(message.tabID, true)
578-
this.messenger.sendUpdatePlaceholder(message.tabID, 'Enter the path to your Java installation.')
579-
}
584+
private async processCustomDependencyVersionFile(message: any) {
585+
const fileUri = await vscode.window.showOpenDialog({
586+
canSelectMany: false,
587+
openLabel: 'Select',
588+
filters: {
589+
'YAML file': ['yaml'], // restrict user to only pick a .yaml file
590+
},
591+
})
592+
if (!fileUri || fileUri.length === 0) {
580593
return
581594
}
595+
const fileContents = await fs.readFileText(fileUri[0].fsPath)
596+
const isValidFile = await validateCustomVersionsFile(fileContents)
582597

583-
await this.prepareLanguageUpgradeProject(message)
598+
if (!isValidFile) {
599+
this.messenger.sendUnrecoverableErrorResponse('invalid-custom-versions-file', message.tabID)
600+
return
601+
}
602+
this.messenger.sendMessage('Received custom dependency version YAML file.', message.tabID, 'ai-prompt')
603+
transformByQState.setCustomDependencyVersionFilePath(fileUri[0].fsPath)
604+
this.promptJavaHome('source', message.tabID)
584605
}
585606

586607
private async processMetadataFile(message: any) {
@@ -657,19 +678,34 @@ export class GumbyController {
657678
}
658679

659680
private async processHumanChatMessage(data: { message: string; tabID: string }) {
660-
this.messenger.sendUserPrompt(data.message, data.tabID)
681+
this.messenger.sendMessage(data.message, data.tabID, 'prompt')
661682
this.messenger.sendChatInputEnabled(data.tabID, false)
662-
this.messenger.sendUpdatePlaceholder(data.tabID, 'Open a new tab to chat with Q')
683+
this.messenger.sendUpdatePlaceholder(data.tabID, CodeWhispererConstants.openNewTabPlaceholder)
663684

664685
const session = this.sessionStorage.getSession()
665686
switch (session.conversationState) {
666-
case ConversationState.PROMPT_JAVA_HOME: {
687+
case ConversationState.PROMPT_SOURCE_JAVA_HOME: {
667688
const pathToJavaHome = extractPath(data.message)
668689
if (pathToJavaHome) {
669-
await this.prepareLanguageUpgradeProject({
670-
pathToJavaHome,
671-
tabID: data.tabID,
672-
})
690+
transformByQState.setSourceJavaHome(pathToJavaHome)
691+
// if source and target JDK versions are the same, just re-use the source JAVA_HOME and start the build
692+
if (transformByQState.getTargetJDKVersion() === transformByQState.getSourceJDKVersion()) {
693+
transformByQState.setTargetJavaHome(pathToJavaHome)
694+
await this.prepareLanguageUpgradeProject(data.tabID)
695+
} else {
696+
this.promptJavaHome('target', data.tabID)
697+
}
698+
} else {
699+
this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID)
700+
}
701+
break
702+
}
703+
704+
case ConversationState.PROMPT_TARGET_JAVA_HOME: {
705+
const pathToJavaHome = extractPath(data.message)
706+
if (pathToJavaHome) {
707+
transformByQState.setTargetJavaHome(pathToJavaHome)
708+
await this.prepareLanguageUpgradeProject(data.tabID) // build right after we get target JDK path
673709
} else {
674710
this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID)
675711
}
@@ -747,7 +783,7 @@ export class GumbyController {
747783
})
748784
}
749785

750-
this.messenger.sendStaticTextResponse('end-HIL-early', message.tabID)
786+
this.messenger.sendMessage(CodeWhispererConstants.continueWithoutHilMessage, message.tabID, 'ai-prompt')
751787
}
752788
}
753789

0 commit comments

Comments
 (0)