Skip to content

Commit bbfc738

Browse files
committed
Merge branch 'main' of https://github.com/microsoft/vscode-azurefunctions into mwf/durable-feedback
2 parents 532564e + 3bb06ae commit bbfc738

File tree

10 files changed

+342
-203
lines changed

10 files changed

+342
-203
lines changed

.github/copilot-instructions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Coding Instructions for GitHub Copilot
2+
3+
- Do not commit or suggest changes to `main.js` when those changes are automatically generated as part of the webpack build process.

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,3 @@ resources/backupTemplates/dotnet/**/cache/
7373

7474
# macOS related extra files
7575
.DS_Store
76-
77-
# Copilot instructions (internal development documentation)
78-
.github/copilot-instructions.md

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/createFunction/durableSteps/DurableProjectConfigureStep.ts

Lines changed: 147 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { AzExtFsExtra, AzureWizardExecuteStepWithActivityOutput } from '@microsoft/vscode-azext-utils';
6+
import { AzExtFsExtra, AzureWizardExecuteStepWithActivityOutput, parseError, type IParsedError } from '@microsoft/vscode-azext-utils';
77
import * as path from "path";
88
import { type Progress } from 'vscode';
99
import { ConnectionKey, DurableBackend, hostFileName, ProjectLanguage } from '../../../constants';
1010
import { viewOutput } from '../../../constants-nls';
1111
import { ext } from '../../../extensionVariables';
12-
import { type IHostJsonV2 } from '../../../funcConfig/host';
12+
import { type IDTSTaskJson, type IHostJsonV2, type INetheriteTaskJson, type ISqlTaskJson, type IStorageTaskJson } from '../../../funcConfig/host';
1313
import { MismatchBehavior, setLocalAppSetting } from '../../../funcConfig/local.settings';
1414
import { localize } from '../../../localize';
15+
import { cpUtils } from '../../../utils/cpUtils';
1516
import { durableUtils } from '../../../utils/durableUtils';
17+
import { pythonUtils } from '../../../utils/pythonUtils';
18+
import { venvUtils } from '../../../utils/venvUtils';
1619
import { type IFunctionWizardContext } from '../IFunctionWizardContext';
1720

1821
export class DurableProjectConfigureStep<T extends IFunctionWizardContext> extends AzureWizardExecuteStepWithActivityOutput<T> {
@@ -25,22 +28,22 @@ export class DurableProjectConfigureStep<T extends IFunctionWizardContext> exten
2528
protected getOutputLogFail(_context: T): string {
2629
return localize('failedToConfigureDurableProject', 'Failed to configure durable project settings.');
2730
}
28-
protected getOutputLogProgress(_context: T): string {
29-
return localize('configuringDurableProject', 'Configuring durable project settings...');
30-
}
3131
protected preDeployTask: string = 'funcHostStart';
3232
public stepName: string = 'DurableProjectConfigureStep';
3333
public priority: number = 225;
3434

35-
public async execute(context: T, _progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
35+
public async execute(context: T, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
36+
progress.report({ message: localize('configuringDurableProject', 'Configuring durable project settings...') });
3637
await this.configureHostAndLocalSettingsJson(context);
37-
await durableUtils.tryInstallDurableDependencies(context);
38+
await this.tryInstallDurableDependencies(context);
3839
}
3940

4041
public shouldExecute(context: T): boolean {
4142
return !!context.newDurableStorageType;
4243
}
4344

45+
// #region Durable Task Local Settings
46+
4447
private async configureHostAndLocalSettingsJson(context: T): Promise<void> {
4548
const hostJsonPath: string = path.join(context.projectPath, hostFileName);
4649

@@ -65,15 +68,15 @@ export class DurableProjectConfigureStep<T extends IFunctionWizardContext> exten
6568

6669
switch (context.newDurableStorageType) {
6770
case DurableBackend.Storage:
68-
hostJson.extensions.durableTask = durableUtils.getDefaultStorageTaskConfig();
71+
hostJson.extensions.durableTask = this.getDefaultStorageTaskConfig();
6972
// Omit setting azureWebJobsStorage since it should already be initialized during 'createNewProject'
7073
break;
7174
case DurableBackend.Netherite:
72-
hostJson.extensions.durableTask = durableUtils.getDefaultNetheriteTaskConfig();
75+
hostJson.extensions.durableTask = this.getDefaultNetheriteTaskConfig();
7376
await setLocalAppSetting(context, context.projectPath, ConnectionKey.EventHubs, '', MismatchBehavior.Overwrite);
7477
break;
7578
case DurableBackend.DTS:
76-
hostJson.extensions.durableTask = durableUtils.getDefaultDTSTaskConfig();
79+
hostJson.extensions.durableTask = this.getDefaultDTSTaskConfig();
7780
// Non- .NET projects require a special preview extension bundle to work properly
7881
// Todo: Remove once this functionality is out of preview
7982
if (context.language !== ProjectLanguage.CSharp && context.language !== ProjectLanguage.FSharp) {
@@ -84,15 +87,147 @@ export class DurableProjectConfigureStep<T extends IFunctionWizardContext> exten
8487
ext.outputChannel.appendLog(localize('extensionBundlePreview', 'Updated "host.json" extension bundle to preview version to enable new DTS features.'));
8588
}
8689
await setLocalAppSetting(context, context.projectPath, ConnectionKey.DTS, '', MismatchBehavior.Overwrite);
87-
await setLocalAppSetting(context, context.projectPath, ConnectionKey.DTSHub, 'default', MismatchBehavior.Overwrite);
90+
await setLocalAppSetting(context, context.projectPath, ConnectionKey.DTSHub, '', MismatchBehavior.Overwrite);
8891
break;
8992
case DurableBackend.SQL:
90-
hostJson.extensions.durableTask = durableUtils.getDefaultSqlTaskConfig();
93+
hostJson.extensions.durableTask = this.getDefaultSqlTaskConfig();
9194
await setLocalAppSetting(context, context.projectPath, ConnectionKey.SQL, '', MismatchBehavior.Overwrite);
9295
break;
9396
default:
9497
}
9598

9699
await AzExtFsExtra.writeJSON(hostJsonPath, hostJson);
97100
}
101+
102+
private getDefaultStorageTaskConfig(): IStorageTaskJson {
103+
return {
104+
storageProvider: {
105+
type: DurableBackend.Storage,
106+
}
107+
};
108+
}
109+
110+
private getDefaultNetheriteTaskConfig(): INetheriteTaskJson {
111+
return {
112+
hubName: '',
113+
useGracefulShutdown: true,
114+
storageProvider: {
115+
type: DurableBackend.Netherite,
116+
StorageConnectionName: ConnectionKey.Storage,
117+
EventHubsConnectionName: ConnectionKey.EventHubs,
118+
}
119+
};
120+
}
121+
122+
private getDefaultDTSTaskConfig(): IDTSTaskJson {
123+
return {
124+
hubName: '%TASKHUB_NAME%',
125+
storageProvider: {
126+
type: DurableBackend.DTS,
127+
connectionStringName: ConnectionKey.DTS,
128+
}
129+
};
130+
}
131+
132+
private getDefaultSqlTaskConfig(): ISqlTaskJson {
133+
return {
134+
storageProvider: {
135+
type: DurableBackend.SQL,
136+
connectionStringName: ConnectionKey.SQL,
137+
taskEventLockTimeout: "00:02:00",
138+
createDatabaseIfNotExists: true,
139+
}
140+
};
141+
}
142+
143+
// #endregion Durable Task Local Settings
144+
145+
// #region Install Durable Dependencies
146+
147+
private async tryInstallDurableDependencies(context: IFunctionWizardContext): Promise<void> {
148+
switch (context.language) {
149+
case ProjectLanguage.Java:
150+
// Todo: Revisit when adding Java implementation
151+
break;
152+
case ProjectLanguage.CSharp:
153+
case ProjectLanguage.FSharp:
154+
await this.installDotnetDependencies(context);
155+
break;
156+
case ProjectLanguage.JavaScript:
157+
case ProjectLanguage.TypeScript:
158+
await this.installNodeDependencies(context);
159+
break;
160+
case ProjectLanguage.Python:
161+
await pythonUtils.addDependencyToRequirements(durableUtils.pythonDfPackage, context.projectPath);
162+
await venvUtils.runPipInstallCommandIfPossible(context.projectPath);
163+
break;
164+
case ProjectLanguage.PowerShell:
165+
// Todo: Revisit when adding PowerShell implementation
166+
break;
167+
default:
168+
}
169+
}
170+
171+
private async installDotnetDependencies(context: IFunctionWizardContext): Promise<void> {
172+
const packages: { name: string; prerelease?: boolean }[] = [];
173+
const isDotnetIsolated: boolean = /Isolated/i.test(context.functionTemplate?.id ?? '');
174+
175+
switch (context.newDurableStorageType) {
176+
case DurableBackend.Netherite:
177+
isDotnetIsolated ?
178+
packages.push({ name: durableUtils.dotnetIsolatedDfNetheritePackage }) :
179+
packages.push({ name: durableUtils.dotnetInProcDfNetheritePackage });
180+
break;
181+
case DurableBackend.DTS:
182+
// Todo: Remove prerelease flag once this functionality is out of preview
183+
isDotnetIsolated ?
184+
packages.push({ name: durableUtils.dotnetIsolatedDTSPackage, prerelease: true }) :
185+
packages.push({ name: durableUtils.dotnetInProcDTSPackage, prerelease: true });
186+
break;
187+
case DurableBackend.SQL:
188+
isDotnetIsolated ?
189+
packages.push({ name: durableUtils.dotnetIsolatedDfSqlPackage }) :
190+
packages.push({ name: durableUtils.dotnetInProcDfSqlPackage });
191+
break;
192+
case DurableBackend.Storage:
193+
default:
194+
}
195+
196+
// Although the templates should incorporate this package already, it is often included with an out-dated version
197+
// which can lead to errors on first run. To improve this experience for our users, ensure that the latest version is used.
198+
if (!isDotnetIsolated) {
199+
packages.push({ name: durableUtils.dotnetInProcDfBasePackage });
200+
}
201+
202+
const failedPackages: string[] = [];
203+
for (const p of packages) {
204+
try {
205+
const packageArgs: string[] = [p.name];
206+
if (p.prerelease) {
207+
packageArgs.push('--prerelease');
208+
}
209+
await cpUtils.executeCommand(ext.outputChannel, context.projectPath, 'dotnet', 'add', 'package', ...packageArgs);
210+
} catch {
211+
failedPackages.push(p.name);
212+
}
213+
}
214+
215+
if (failedPackages.length) {
216+
ext.outputChannel.appendLog(localize('durableDependencyInstallFailed', 'WARNING: Failed to install and update Durable Functions NuGet packages to the root .csproj project file. You may need to install the following packages manually: "{0}".', failedPackages.join('", "')));
217+
}
218+
}
219+
220+
private async installNodeDependencies(context: IFunctionWizardContext): Promise<void> {
221+
try {
222+
const packageVersion = context.languageModel === 4 ? '3' : '2';
223+
await cpUtils.executeCommand(ext.outputChannel, context.projectPath, 'npm', 'install', `${durableUtils.nodeDfPackage}@${packageVersion}`);
224+
} catch (error) {
225+
const pError: IParsedError = parseError(error);
226+
const dfDepInstallFailed: string = localize('failedToAddDurableNodeDependency', 'Failed to add or install the "{0}" dependency. Please inspect and verify if it needs to be added manually.', durableUtils.nodeDfPackage);
227+
ext.outputChannel.appendLog(pError.message);
228+
ext.outputChannel.appendLog(dfDepInstallFailed);
229+
}
230+
}
231+
232+
// #endregion Install Durable Dependencies
98233
}
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+
}

0 commit comments

Comments
 (0)