Skip to content

Commit 0971f2a

Browse files
authored
Deploy with the Function CLI if it is installed (#4630)
* Fix template version file null/undefined error Fixes #4599 - Cannot read properties of undefined (reading 'trim') when backup template version file read returns null/undefined. Added null check before calling toString().trim() on the file content to provide a more actionable error message when backup version files are missing or empty. * Deploy with Func CLI * Fix line break * Delete .github/copilot-instructions.md * Delete example-usage.md * Update TemplateProviderBase.ts * Account for deployment slots
1 parent 89c6f67 commit 0971f2a

File tree

4 files changed

+183
-40
lines changed

4 files changed

+183
-40
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type InnerDeployContext } from "@microsoft/vscode-azext-azureappservice";
7+
import { ActivityChildItem, ActivityChildType, activityFailContext, activityFailIcon, activityProgressContext, activityProgressIcon, activitySuccessContext, activitySuccessIcon, AzureWizardExecuteStep, createContextValue, randomUtils, type ExecuteActivityOutput } from "@microsoft/vscode-azext-utils";
8+
import { l10n, ThemeIcon, TreeItemCollapsibleState, type Progress } from "vscode";
9+
import { ext } from "../../extensionVariables";
10+
import { cpUtils } from "../../utils/cpUtils";
11+
12+
export class DeployFunctionCoreToolsStep extends AzureWizardExecuteStep<InnerDeployContext> {
13+
stepName: string;
14+
private _childId: string = randomUtils.getRandomHexString(8); // create child id in class to make it idempotent
15+
private _command: { title: string; command: string } = {
16+
title: '',
17+
command: ext.prefix + '.showOutputChannel'
18+
};
19+
public createSuccessOutput(context: InnerDeployContext): ExecuteActivityOutput {
20+
const label = l10n.t('Publish "{0}" to "{1}" with Function Core Tools', context.originalDeployFsPath, context.site.fullName);
21+
return {
22+
item: new ActivityChildItem({
23+
contextValue: createContextValue([activitySuccessContext, context.site.id]),
24+
label,
25+
iconPath: activitySuccessIcon,
26+
activityType: ActivityChildType.Success,
27+
28+
})
29+
};
30+
}
31+
public createProgressOutput(context: InnerDeployContext): ExecuteActivityOutput {
32+
const label = l10n.t('Publish "{0}" to "{1}" with Function Core Tools', context.originalDeployFsPath, context.site.fullName);
33+
const item = new ActivityChildItem({
34+
contextValue: createContextValue([activityProgressContext, context.site.id]),
35+
label,
36+
iconPath: activityProgressIcon,
37+
activityType: ActivityChildType.Progress,
38+
isParent: true,
39+
initialCollapsibleState: TreeItemCollapsibleState.Expanded
40+
});
41+
42+
item.getChildren = () => {
43+
return [
44+
new ActivityChildItem({
45+
label: l10n.t('Click to view output channel'),
46+
id: this._childId,
47+
command: this._command,
48+
activityType: ActivityChildType.Info,
49+
contextValue: createContextValue([activityProgressContext, 'viewOutputChannel']),
50+
iconPath: new ThemeIcon('output')
51+
})
52+
];
53+
};
54+
55+
return {
56+
item
57+
};
58+
}
59+
public createFailOutput(context: InnerDeployContext): ExecuteActivityOutput {
60+
const label = l10n.t('Publish "{0}" to "{1}" with Function Core Tools', context.originalDeployFsPath, context.site.fullName);
61+
const item = new ActivityChildItem({
62+
contextValue: createContextValue([activityFailContext, context.site.id]),
63+
label,
64+
iconPath: activityFailIcon,
65+
activityType: ActivityChildType.Fail,
66+
isParent: true,
67+
initialCollapsibleState: TreeItemCollapsibleState.Expanded
68+
});
69+
70+
item.getChildren = () => {
71+
return [
72+
new ActivityChildItem({
73+
label: l10n.t('Click to view output channel'),
74+
id: this._childId,
75+
command: this._command,
76+
activityType: ActivityChildType.Info,
77+
contextValue: createContextValue([activityProgressContext, 'viewOutputChannel']),
78+
iconPath: new ThemeIcon('output')
79+
})
80+
];
81+
};
82+
83+
return {
84+
item
85+
};
86+
}
87+
public priority: number = 100;
88+
public async execute(context: InnerDeployContext, progress: Progress<{ message?: string; increment?: number; }>): Promise<void> {
89+
const message = l10n.t('Publishing "{0}" to "{1}" with Functiontion Core Tools...', context.originalDeployFsPath, context.site.fullName);
90+
progress.report({ message });
91+
context.activityAttributes = context.activityAttributes ?? { logs: [] };
92+
const args = ['func', 'azure', 'functionapp', 'publish', context.site.siteName];
93+
if (context.site.isSlot) {
94+
// if there's no slotName, then just assume production
95+
args.push('--slot', context.site.slotName ?? 'production');
96+
}
97+
const cmdOutput = await cpUtils.tryExecuteCommand(ext.outputChannel, context.originalDeployFsPath, args.join(' '));
98+
context.activityAttributes.logs = [{ content: cmdOutput.cmdOutputIncludingStderr }];
99+
}
100+
public shouldExecute(_context: InnerDeployContext): boolean {
101+
return true;
102+
}
103+
}

src/commands/deploy/deploy.ts

Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { type Site, type SiteConfigResource, type StringDictionary } from '@azure/arm-appservice';
7-
import { getDeployFsPath, getDeployNode, deploy as innerDeploy, showDeployConfirmation, type IDeployContext, type IDeployPaths, type ParsedSite } from '@microsoft/vscode-azext-azureappservice';
7+
import { getDeployFsPath, getDeployNode, deploy as innerDeploy, showDeployConfirmation, type IDeployContext, type IDeployPaths, type InnerDeployContext, type ParsedSite } from '@microsoft/vscode-azext-azureappservice';
88
import { ResourceGroupListStep } from '@microsoft/vscode-azext-azureutils';
9-
import { DialogResponses, subscriptionExperience, type ExecuteActivityContext, type IActionContext, type ISubscriptionContext } from '@microsoft/vscode-azext-utils';
9+
import { AzureWizard, DialogResponses, subscriptionExperience, type ExecuteActivityContext, type IActionContext, type ISubscriptionContext } from '@microsoft/vscode-azext-utils';
1010
import { type AzureSubscription } from '@microsoft/vscode-azureresources-api';
1111
import type * as vscode from 'vscode';
1212
import { CodeAction, deploySubpathSetting, DurableBackend, hostFileName, ProjectLanguage, remoteBuildSetting, ScmType, stackUpgradeLearnMoreLink } from '../../constants';
1313
import { ext } from '../../extensionVariables';
1414
import { addLocalFuncTelemetry } from '../../funcCoreTools/getLocalFuncCoreToolsVersion';
15+
import { funcToolsInstalled, validateFuncCoreToolsInstalled } from '../../funcCoreTools/validateFuncCoreToolsInstalled';
1516
import { localize } from '../../localize';
1617
import { ResolvedFunctionAppResource } from '../../tree/ResolvedFunctionAppResource';
1718
import { type SlotTreeItem } from '../../tree/SlotTreeItem';
@@ -30,6 +31,7 @@ import { getNetheriteConnectionIfNeeded } from '../appSettings/connectionSetting
3031
import { getSQLConnectionIfNeeded } from '../appSettings/connectionSettings/sqlDatabase/getSQLConnection';
3132
import { getEolWarningMessages } from '../createFunctionApp/stacks/getStackPicks';
3233
import { tryGetFunctionProjectRoot } from '../createNewProject/verifyIsProject';
34+
import { DeployFunctionCoreToolsStep } from './DeployFunctionCoreToolsStep';
3335
import { getOrCreateFunctionApp } from './getOrCreateFunctionApp';
3436
import { getWarningsForConnectionSettings } from './getWarningsForConnectionSettings';
3537
import { notifyDeployComplete } from './notifyDeployComplete';
@@ -218,54 +220,81 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
218220
eolWarningMessage ? stackUpgradeLearnMoreLink : undefined);
219221
}
220222

221-
await runPreDeployTask(context, context.effectiveDeployFsPath, siteConfig.scmType);
222-
223-
if (isZipDeploy) {
224-
void validateGlobSettings(context, context.effectiveDeployFsPath);
223+
let isFuncToolsInstalled: boolean = await funcToolsInstalled(context, context.workspaceFolder.uri.fsPath);
224+
if (language === ProjectLanguage.Custom && !isFuncToolsInstalled) {
225+
await validateFuncCoreToolsInstalled(context, localize('validateFuncCoreToolsCustom', 'The Functions Core Tools are required to deploy to a custom runtime function app.'));
226+
isFuncToolsInstalled = true;
225227
}
226228

227-
if (language === ProjectLanguage.CSharp && !site.isLinux || durableStorageType) {
228-
await updateWorkerProcessTo64BitIfRequired(context, siteConfig, site, language, durableStorageType);
229-
}
230229

231-
// app settings shouldn't be checked with flex consumption plans
232-
if (isZipDeploy && !isFlexConsumption) {
233-
await verifyAppSettings({
234-
context,
235-
node,
236-
projectPath: context.projectPath,
237-
version,
238-
language,
239-
languageModel,
240-
bools: { doRemoteBuild, isConsumption },
241-
durableStorageType,
242-
appSettings
243-
});
244-
}
230+
if (!isFuncToolsInstalled) {
231+
await runPreDeployTask(context, context.effectiveDeployFsPath, siteConfig.scmType);
232+
233+
if (isZipDeploy) {
234+
void validateGlobSettings(context, context.effectiveDeployFsPath);
235+
}
236+
237+
if (language === ProjectLanguage.CSharp && !site.isLinux || durableStorageType) {
238+
await updateWorkerProcessTo64BitIfRequired(context, siteConfig, site, language, durableStorageType);
239+
}
245240

241+
// app settings shouldn't be checked with flex consumption plans
242+
if (isZipDeploy && !isFlexConsumption) {
243+
await verifyAppSettings({
244+
context,
245+
node,
246+
projectPath: context.projectPath,
247+
version,
248+
language,
249+
languageModel,
250+
bools: { doRemoteBuild, isConsumption },
251+
durableStorageType,
252+
appSettings
253+
});
254+
}
255+
}
256+
let deployedWithFuncCli = false;
246257
await node.runWithTemporaryDescription(
247258
context,
248259
localize('deploying', 'Deploying...'),
249260
async () => {
250-
// Stop function app here to avoid *.jar file in use on server side.
251-
// More details can be found: https://github.com/Microsoft/vscode-azurefunctions/issues/106
252-
context.stopAppBeforeDeploy = language === ProjectLanguage.Java;
253-
254-
// preDeploy tasks are only required for zipdeploy so subpath may not exist
255-
let deployFsPath: string = context.effectiveDeployFsPath;
256-
257-
if (!isZipDeploy && !isPathEqual(context.effectiveDeployFsPath, context.originalDeployFsPath)) {
258-
deployFsPath = context.originalDeployFsPath;
259-
const noSubpathWarning: string = `WARNING: Ignoring deploySubPath "${getWorkspaceSetting(deploySubpathSetting, context.originalDeployFsPath)}" for non-zip deploy.`;
260-
ext.outputChannel.appendLog(noSubpathWarning);
261+
// prioritize func cli deployment if installed
262+
if (isFuncToolsInstalled) {
263+
context.telemetry.properties.funcCoreToolsInstalled = 'true';
264+
context.telemetry.properties.deployMethod = 'funccli';
265+
const deployContext = Object.assign(context, await createActivityContext(), { site }) as unknown as InnerDeployContext;
266+
deployContext.activityChildren = [];
267+
const wizard = new AzureWizard(deployContext, {
268+
executeSteps: [new DeployFunctionCoreToolsStep()],
269+
});
270+
271+
deployContext.activityTitle = site.isSlot
272+
? localize('deploySlot', 'Deploy to slot "{0}"', site.fullName)
273+
: localize('deployApp', 'Deploy to app "{0}"', site.fullName);
274+
await wizard.execute();
275+
deployedWithFuncCli = true;
276+
return;
277+
} else {
278+
// Stop function app here to avoid *.jar file in use on server side.
279+
// More details can be found: https://github.com/Microsoft/vscode-azurefunctions/issues/106
280+
context.stopAppBeforeDeploy = language === ProjectLanguage.Java;
281+
282+
// preDeploy tasks are only required for zipdeploy so subpath may not exist
283+
let deployFsPath: string = context.effectiveDeployFsPath;
284+
285+
if (!isZipDeploy && !isPathEqual(context.effectiveDeployFsPath, context.originalDeployFsPath)) {
286+
deployFsPath = context.originalDeployFsPath;
287+
const noSubpathWarning: string = `WARNING: Ignoring deploySubPath "${getWorkspaceSetting(deploySubpathSetting, context.originalDeployFsPath)}" for non-zip deploy.`;
288+
ext.outputChannel.appendLog(noSubpathWarning);
289+
}
290+
const deployContext = Object.assign(context, await createActivityContext());
291+
deployContext.activityChildren = [];
292+
await innerDeploy(site, deployFsPath, deployContext);
261293
}
262-
const deployContext = Object.assign(context, await createActivityContext());
263-
deployContext.activityChildren = [];
264-
await innerDeploy(site, deployFsPath, deployContext);
265294
}
266295
);
267296

268-
await notifyDeployComplete(context, node, context.workspaceFolder, isFlexConsumption);
297+
await notifyDeployComplete(context, node, context.workspaceFolder, isFlexConsumption, deployedWithFuncCli);
269298
}
270299

271300
async function updateWorkerProcessTo64BitIfRequired(context: IDeployContext, siteConfig: SiteConfigResource, site: ParsedSite, language: ProjectLanguage, durableStorageType: DurableBackend | undefined): Promise<void> {
@@ -310,3 +339,4 @@ async function validateGlobSettings(context: IActionContext, fsPath: string): Pr
310339
await context.ui.showWarningMessage(message, { stepName: 'globSettingRemoved' });
311340
}
312341
}
342+

src/commands/deploy/notifyDeployComplete.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { type IDeployContext } from '@microsoft/vscode-azext-azureappservice';
67
import { callWithTelemetryAndErrorHandling, type AzExtTreeItem, type IActionContext } from '@microsoft/vscode-azext-utils';
78
import * as retry from 'p-retry';
89
import { window, type MessageItem, type WorkspaceFolder } from 'vscode';
@@ -16,7 +17,7 @@ import { uploadAppSettings } from '../appSettings/uploadAppSettings';
1617
import { startStreamingLogs } from '../logstream/startStreamingLogs';
1718
import { hasRemoteEventGridBlobTrigger, promptForEventGrid } from './promptForEventGrid';
1819

19-
export async function notifyDeployComplete(context: IActionContext, node: SlotTreeItem, workspaceFolder: WorkspaceFolder, isFlexConsumption?: boolean): Promise<void> {
20+
export async function notifyDeployComplete(context: IDeployContext, node: SlotTreeItem, workspaceFolder: WorkspaceFolder, isFlexConsumption?: boolean, deployedWithFuncCli?: boolean): Promise<void> {
2021
await node.initSite(context);
2122
const deployComplete: string = localize('deployComplete', 'Deployment to "{0}" completed.', node.site.fullName);
2223
const viewOutput: MessageItem = { title: localize('viewOutput', 'View output') };
@@ -55,6 +56,10 @@ export async function notifyDeployComplete(context: IActionContext, node: SlotTr
5556
});
5657

5758
try {
59+
if (deployedWithFuncCli) {
60+
// don't query triggers if we used the func cli to deploy
61+
return;
62+
}
5863
const retries: number = 4;
5964
await retry(
6065
async (currentAttempt: number) => {

src/templates/TemplateProviderBase.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { type IProjectWizardContext } from '../commands/createNewProject/IProjec
1111
import { type ProjectLanguage } from '../constants';
1212
import { NotImplementedError } from '../errors';
1313
import { ext } from '../extensionVariables';
14+
import { localize } from '../localize';
1415
import { type IBindingTemplate } from './IBindingTemplate';
1516
import { type FunctionTemplateBase } from './IFunctionTemplate';
1617
import { type ITemplates } from './ITemplates';
@@ -106,7 +107,11 @@ export abstract class TemplateProviderBase implements Disposable {
106107
}
107108

108109
public async getBackupTemplateVersion(): Promise<string> {
109-
return (await AzExtFsExtra.readFile(await this.getBackupVersionPath())).toString().trim();
110+
const versionContent = await AzExtFsExtra.readFile(await this.getBackupVersionPath());
111+
if (!versionContent) {
112+
throw new Error(localize('backupVersionFileEmpty', 'Backup template version file is empty or could not be read'));
113+
}
114+
return versionContent.toString().trim();
110115
}
111116

112117
public async updateBackupTemplateVersion(version: string): Promise<void> {

0 commit comments

Comments
 (0)