Skip to content

Commit cd4f400

Browse files
authored
feat(sam): remove "code only" experiment, add --no-watch #3284
Problem: * SAM CLI 1.78.0 includes a "smarter" `sync`, making the "code only" experimental option redundant. * Specifying the `watch` flag in `samconfig.toml` causes an unexpected UX. Solution: * Remove the "code only" experiment. * Add `--no-watch` if applicable (for older SAM CLI). * Add one-time notification to update SAM CLI for perf improvements. * Only shows when running "Sync Serverless Application (formerly Deploy)". * Create ECR repo if it doesn't exist.
1 parent 0658015 commit cd4f400

File tree

9 files changed

+78
-51
lines changed

9 files changed

+78
-51
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Breaking Change",
3+
"description": "\"Sync SAM Application\" will now always ignore the 'watch' flag in `samconfig.toml`. The Toolkit does not support running `sam sync` in 'watch' mode."
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Removal",
3+
"description": "`aws.experiments.samSyncCode` has been removed as similiar functionality is now in SAM CLI by default in 1.78.0."
4+
}

package.json

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -224,17 +224,12 @@
224224
"type": "object",
225225
"markdownDescription": "%AWS.configuration.description.experiments%",
226226
"default": {
227-
"jsonResourceModification": false,
228-
"samSyncCode": false
227+
"jsonResourceModification": false
229228
},
230229
"properties": {
231230
"jsonResourceModification": {
232231
"type": "boolean",
233232
"default": false
234-
},
235-
"samSyncCode": {
236-
"type": "boolean",
237-
"default": false
238233
}
239234
},
240235
"additionalProperties": false
@@ -951,10 +946,6 @@
951946
"command": "aws.samcli.sync",
952947
"when": "!config.aws.samcli.legacyDeploy"
953948
},
954-
{
955-
"command": "aws.samcli.syncCode",
956-
"when": "config.aws.experiments.samSyncCode && !config.aws.samcli.legacyDeploy"
957-
},
958949
{
959950
"command": "aws.s3.copyPath",
960951
"when": "false"
@@ -1194,11 +1185,6 @@
11941185
"when": "!config.aws.samcli.legacyDeploy && view == aws.explorer",
11951186
"group": "3_lambda@3"
11961187
},
1197-
{
1198-
"command": "aws.samcli.syncCode",
1199-
"when": "config.aws.experiments.samSyncCode && !config.aws.samcli.legacyDeploy && view == aws.explorer",
1200-
"group": "3_lambda@4"
1201-
},
12021188
{
12031189
"command": "aws.quickStart",
12041190
"when": "view == aws.explorer",
@@ -1266,11 +1252,6 @@
12661252
"when": "!config.aws.samcli.legacyDeploy && isFileSystemResource && resourceFilename =~ /^(template\\.(json|yml|yaml))|(samconfig\\.toml)$/",
12671253
"group": "z_aws@1"
12681254
},
1269-
{
1270-
"command": "aws.samcli.syncCode",
1271-
"when": "config.aws.experiments.samSyncCode && !config.aws.samcli.legacyDeploy && isFileSystemResource && resourceFilename =~ /^(template\\.(json|yml|yaml))|(samconfig\\.toml)$/",
1272-
"group": "z_aws@2"
1273-
},
12741255
{
12751256
"command": "aws.uploadLambda",
12761257
"when": "explorerResourceIsFolder || isFileSystemResource && resourceFilename =~ /^template\\.(json|yml|yaml)$/",
@@ -1383,11 +1364,6 @@
13831364
"when": "!config.aws.samcli.legacyDeploy && view == aws.explorer && viewItem =~ /^(awsLambdaNode|awsRegionNode|awsCloudFormationRootNode)$/",
13841365
"group": "1@2"
13851366
},
1386-
{
1387-
"command": "aws.samcli.syncCode",
1388-
"when": "config.aws.experiments.samSyncCode && !config.aws.samcli.legacyDeploy && view == aws.explorer && viewItem =~ /^(awsLambdaNode|awsRegionNode|awsCloudFormationRootNode)$/",
1389-
"group": "1@3"
1390-
},
13911367
{
13921368
"command": "aws.ecr.copyTagUri",
13931369
"when": "view == aws.explorer && viewItem == awsEcrTagNode",
@@ -3060,11 +3036,6 @@
30603036
"title": "%AWS.command.samcli.sync%",
30613037
"category": "%AWS.title%"
30623038
},
3063-
{
3064-
"command": "aws.samcli.syncCode",
3065-
"title": "%AWS.command.samcli.syncCode%",
3066-
"category": "%AWS.title%"
3067-
},
30683039
{
30693040
"command": "aws.codeWhisperer",
30703041
"title": "%AWS.command.codewhisperer.title%",

package.nls.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,7 @@
146146
"AWS.command.iot.setDefaultPolicy": "Set as Default",
147147
"AWS.command.iot.viewPolicyVersion": "View...",
148148
"AWS.command.iot.copyEndpoint": "Copy Endpoint...",
149-
"AWS.command.samcli.sync": "Sync SAM Application",
150-
"AWS.command.samcli.syncCode": "Sync SAM Application (code only)",
149+
"AWS.command.samcli.sync": "Sync SAM Application (formerly Deploy)",
151150
"AWS.command.s3.downloadFileAs": "Download As...",
152151
"AWS.command.s3.editFile": "Edit File",
153152
"AWS.command.s3.openFile": "Open File",

src/shared/clients/ecrClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ export class DefaultEcrClient {
6767
return collection.filter(isNonNullable).map(list => list.map(repo => (assertHasProps(repo), repo)))
6868
}
6969

70-
public async createRepository(repositoryName: string): Promise<void> {
70+
public async createRepository(repositoryName: string) {
7171
const sdkClient = await this.createSdkClient()
72-
await sdkClient.createRepository({ repositoryName: repositoryName }).promise()
72+
return sdkClient.createRepository({ repositoryName: repositoryName }).promise()
7373
}
7474

7575
public async deleteRepository(repositoryName: string): Promise<void> {

src/shared/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export const endpointsFileUrl: string = 'https://idetoolkits.amazonwebservices.c
1717
export const aboutCredentialsFileUrl: string =
1818
'https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html'
1919
export const samAboutInstallUrl = vscode.Uri.parse('https://aws.amazon.com/serverless/sam/')
20+
export const samUpgradeUrl = vscode.Uri.parse(
21+
'https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/manage-sam-cli-versions.html#manage-sam-cli-versions-upgrade'
22+
)
2023
export const vscodeMarketplaceUrl: string =
2124
'https://marketplace.visualstudio.com/items?itemName=AmazonWebServices.aws-toolkit-vscode'
2225
export const githubUrl: string = 'https://github.com/aws/aws-toolkit-vscode'

src/shared/sam/sync.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ import { parse } from 'semver'
4141
import { isAutomation } from '../vscode/env'
4242
import { getOverriddenParameters } from '../../lambda/config/parameterUtils'
4343
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
44-
import { samSyncUrl, samInitDocUrl } from '../constants'
44+
import { samSyncUrl, samInitDocUrl, samUpgradeUrl } from '../constants'
4545
import { getAwsConsoleUrl } from '../awsConsole'
4646
import { openUrl } from '../utilities/vsCodeUtils'
47+
import { showOnce } from '../utilities/messages'
4748

4849
const localize = nls.loadMessageBundle()
4950

@@ -60,6 +61,7 @@ export interface SyncParams {
6061
}
6162

6263
export const prefixNewBucketName = (name: string) => `newbucket:${name}`
64+
export const prefixNewRepoName = (name: string) => `newrepo:${name}`
6365

6466
function createBucketPrompter(client: DefaultS3Client) {
6567
const recentBucket = getRecentResponse(client.regionCode, 'bucketName')
@@ -143,11 +145,11 @@ function createEcrPrompter(client: DefaultEcrClient) {
143145

144146
return createQuickPick(items, {
145147
title: 'Select an ECR Repository',
146-
placeholder: 'Select a repository (or enter repository URI)',
148+
placeholder: 'Select a repository (or enter a name to create one)',
147149
buttons: createCommonButtons(samSyncUrl, consoleUrl),
148150
filterBoxInputSettings: {
149-
label: 'Existing repository URI',
150-
transform: v => v,
151+
label: 'Create a New Repository',
152+
transform: v => prefixNewRepoName(v),
151153
},
152154
noItemsFoundItem: {
153155
label: localize(
@@ -272,6 +274,21 @@ async function ensureBucket(resp: Pick<SyncParams, 'region' | 'bucketName'>) {
272274
}
273275
}
274276

277+
async function ensureRepo(resp: Pick<SyncParams, 'region' | 'ecrRepoUri'>) {
278+
const newRepoName = resp.ecrRepoUri?.match(/^newrepo:(.*)/)?.[1]
279+
if (newRepoName === undefined) {
280+
return resp.ecrRepoUri
281+
}
282+
283+
try {
284+
const repo = await new DefaultEcrClient(resp.region).createRepository(newRepoName)
285+
286+
return repo.repository?.repositoryUri
287+
} catch (err) {
288+
throw ToolkitError.chain(err, `Failed to create new ECR repository "${newRepoName}"`)
289+
}
290+
}
291+
275292
async function injectCredentials(conn: IamConnection, env = process.env) {
276293
const creds = await conn.getCredentials()
277294
return { ...env, ...asEnvironmentVariables(creds) }
@@ -282,7 +299,8 @@ async function saveAndBindArgs(args: SyncParams): Promise<{ readonly boundArgs:
282299
codeOnly: args.deployType === 'code',
283300
templatePath: args.template.uri.fsPath,
284301
bucketName: await ensureBucket(args),
285-
...selectFrom(args, 'stackName', 'ecrRepoUri', 'region', 'skipDependencyLayer'),
302+
ecrRepoUri: await ensureRepo(args),
303+
...selectFrom(args, 'stackName', 'region', 'skipDependencyLayer'),
286304
}
287305

288306
await Promise.all([
@@ -305,20 +323,21 @@ async function saveAndBindArgs(args: SyncParams): Promise<{ readonly boundArgs:
305323
return { boundArgs }
306324
}
307325

308-
async function getSamCliPath() {
326+
async function getSamCliPathAndVersion() {
309327
const { path: samCliPath } = await SamCliSettings.instance.getOrDetectSamCli()
310328
if (samCliPath === undefined) {
311329
throw new ToolkitError('SAM CLI could not be found', { code: 'MissingExecutable' })
312330
}
313331

314332
const info = await new SamCliInfoInvocation(samCliPath).execute()
333+
const parsedVersion = parse(info.version)
315334
telemetry.record({ version: info.version })
316335

317-
if (parse(info.version)?.compare('1.53.0') === -1) {
336+
if (parsedVersion?.compare('1.53.0') === -1) {
318337
throw new ToolkitError('SAM CLI version 1.53.0 or higher is required', { code: 'VersionTooLow' })
319338
}
320339

321-
return samCliPath
340+
return { path: samCliPath, parsedVersion }
322341
}
323342

324343
let oldTerminal: ProcessTerminal | undefined
@@ -380,7 +399,7 @@ async function loadLegacyParameterOverrides(template: TemplateItem) {
380399
export async function runSamSync(args: SyncParams) {
381400
telemetry.record({ lambdaPackageType: args.ecrRepoUri !== undefined ? 'Image' : 'Zip' })
382401

383-
const samCliPath = await getSamCliPath()
402+
const { path: samCliPath, parsedVersion } = await getSamCliPathAndVersion()
384403
const { boundArgs } = await saveAndBindArgs(args)
385404
const overrides = await loadLegacyParameterOverrides(args.template)
386405
if (overrides !== undefined) {
@@ -390,6 +409,23 @@ export async function runSamSync(args: SyncParams) {
390409
boundArgs.push('--parameter-overrides', ...overrides)
391410
}
392411

412+
// '--no-watch' was not added until https://github.com/aws/aws-sam-cli/releases/tag/v1.77.0
413+
// Forcing every user to upgrade will be a headache for what is otherwise a minor problem
414+
if ((parsedVersion?.compare('1.77.0') ?? -1) >= 0) {
415+
boundArgs.push('--no-watch')
416+
}
417+
418+
if ((parsedVersion?.compare('1.78.0') ?? 1) < 0) {
419+
showOnce('sam.sync.updateMessage', async () => {
420+
const openDocsItem = 'Open Upgrade Documentation'
421+
const message = `Your current version of SAM CLI (${parsedVersion?.version}) does not include performance improvements for "sam sync". Update to 1.78.0 or higher for faster executions.`
422+
const resp = await vscode.window.showInformationMessage(message, openDocsItem)
423+
if (resp === openDocsItem) {
424+
await openUrl(samUpgradeUrl)
425+
}
426+
})
427+
}
428+
393429
const sam = new ChildProcess(samCliPath, ['sync', ...boundArgs], {
394430
spawnOptions: await addTelemetryEnvVar({
395431
cwd: args.projectRoot.fsPath,
@@ -516,14 +552,6 @@ export function registerSync() {
516552
(arg?: unknown) => telemetry.sam_sync.run(() => runSync('infra', arg))
517553
)
518554

519-
Commands.register(
520-
{
521-
id: 'aws.samcli.syncCode',
522-
autoconnect: true,
523-
},
524-
(arg?: unknown) => telemetry.sam_sync.run(() => runSync('code', arg))
525-
)
526-
527555
const settings = SamCliSettings.instance
528556
settings.onDidChange(({ key }) => {
529557
if (key === 'legacyDeploy') {

src/shared/utilities/messages.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { sleep } from './timeoutUtils'
1313
import { Timeout } from './timeoutUtils'
1414
import { addCodiconToString } from './textUtilities'
1515
import { getIcon, codicon } from '../icons'
16+
import globals from '../extensionGlobals'
1617

1718
export const messages = {
1819
editCredentials(icon: boolean) {
@@ -215,3 +216,18 @@ export async function copyToClipboard(data: string, label?: string, env: Env = v
215216
vscode.window.setStatusBarMessage(addCodiconToString('clippy', message), 5000)
216217
getLogger().verbose('copied %s to clipboard: %O', label ?? '', data)
217218
}
219+
220+
export async function showOnce<T>(
221+
key: string,
222+
fn: () => Promise<T>,
223+
memento = globals.context.globalState
224+
): Promise<T | undefined> {
225+
if (memento.get(key)) {
226+
return
227+
}
228+
229+
const result = fn()
230+
await memento.update(key, true)
231+
232+
return result
233+
}

src/test/ecr/commands/createRepository.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ describe('createRepositoryCommand', function () {
3030

3131
const stub = sandbox.stub(ecr, 'createRepository').callsFake(async name => {
3232
assert.strictEqual(name, repoName)
33+
34+
return {} as any
3335
})
3436

3537
getTestWindow().onDidShowInputBox(input => {

0 commit comments

Comments
 (0)