Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6771a13
fix UserCanceledError logged as error in telemetry, clean up create u…
andrew-eldridge Aug 17, 2026
ea1e98c
move promptShouldAutoStartDesignTime to main, update promptShouldEnab…
andrew-eldridge Aug 17, 2026
597c5f1
use global state for auto start design time notification suppressed
andrew-eldridge Aug 17, 2026
c225c3d
address pr comments
andrew-eldridge Aug 17, 2026
9e19957
fix startDesignTimeApi result telemetry
andrew-eldridge Aug 17, 2026
5987093
fix infinite loop scenario when orphaned design-time process exists, …
andrew-eldridge Aug 17, 2026
34b3b0c
enforce startDesignTimeApi recursion limit for retries, remove unused…
andrew-eldridge Aug 18, 2026
39fac92
remove unused settings
andrew-eldridge Aug 18, 2026
265cfcf
update remaining suppress notifications to use global state, remove u…
andrew-eldridge Aug 18, 2026
c3a6ea7
fix ensureWorkspace create new workspace: default to current folder f…
andrew-eldridge Aug 19, 2026
0a731f0
fix 'createWorkspaceStructure' defaults not being loaded
andrew-eldridge Aug 19, 2026
c960d2f
fix pathEquals check in createWorkspace validation
andrew-eldridge Aug 19, 2026
85bd1c5
fix path normalization issues
andrew-eldridge Aug 19, 2026
155cdbf
add check for invalid new workspace location in ensureWorkspace
andrew-eldridge Aug 19, 2026
df3cdde
update tests
andrew-eldridge Aug 19, 2026
d8a16a3
fix errors in pick custom code worker process events due to race cond…
andrew-eldridge Aug 20, 2026
1dc6dfe
fix path comparisons in buildCustomCodeFunctionsProject, remove redun…
andrew-eldridge Aug 20, 2026
c71ac65
Merge branch 'main' of github.com:Azure/LogicAppsUX into aeldridge/vs…
andrew-eldridge Aug 20, 2026
c1cec43
update settingsToExclude on deploy
andrew-eldridge Aug 20, 2026
db50a91
add explicit 'Succeeded' back to some commands
andrew-eldridge Aug 20, 2026
f42f2a0
use default .net dependency version when feed unavailable
andrew-eldridge Aug 20, 2026
a65014c
fix(vscode): seed NuGet E2E workspace before open
Copilot Aug 20, 2026
41f3255
fix(templates): mount only the active template panel to avoid stale a…
Copilot Aug 20, 2026
7dbaaca
Revert "fix(templates): mount only the active template panel to avoid…
andrew-eldridge Aug 20, 2026
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
2 changes: 2 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@
"KY5eNe": "On",
"KYX5Do": "Underline (⌘U)",
"KZOa5l": "Cancel",
"Karw2Q": "Workspace location cannot be inside the currently open project folder. Choose the current folder or a location outside it.",
"Kasmd1": "Go to workflow",
"Kb5u9F": "About tab",
"KlDW+5": "(UTC+02:00) Beirut",
Expand Down Expand Up @@ -3099,6 +3100,7 @@
"_KY5eNe.comment": "Label for the enabled switch",
"_KYX5Do.comment": "Command for underline text for Mac users",
"_KZOa5l.comment": "Label for cancel button",
"_Karw2Q.comment": "Workspace location is a descendant of the currently open folder error text",
"_Kasmd1.comment": "Label to indicate go to the workflow",
"_Kb5u9F.comment": "An accessibility label that describes the about tab",
"_KlDW+5.comment": "Time zone value ",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ensureWorkspace } from '../ensureWorkspace';
import { ensureWorkspace, createWorkspaceFile } from '../ensureWorkspace';
import * as vscode from 'vscode';
import * as workspaceUtils from '../../utils/workspace';
import * as verifyProject from '../../utils/verifyIsProject';
Expand Down Expand Up @@ -35,6 +35,18 @@ vi.mock('../shared/workspaceWebviewCommandHandler', () => ({
createWorkspaceWebviewCommandHandler: vi.fn(),
}));

vi.mock('fs-extra', async (importOriginal) => {
const original = await importOriginal<typeof fse>();
return {
...original,
writeJson: vi.fn(),
copy: vi.fn(),
ensureDir: vi.fn(),
readdir: vi.fn(),
pathExists: vi.fn(),
};
});

describe('ensureWorkspace', () => {
const testWorkspaceName = 'TestWorkspace';
const testWorkspaceFolder: vscode.WorkspaceFolder = {
Expand Down Expand Up @@ -133,13 +145,13 @@ describe('ensureWorkspace', () => {
vi.spyOn(verifyProject, 'getFirstLogicAppProjectRoot').mockImplementation(async (f: vscode.WorkspaceFolder | string | undefined) => {
return f === testLogicAppChildFolder ? testLogicAppChildFolder : undefined;
});
vi.spyOn(fse, 'readdir').mockImplementation(async (p: fse.PathLike) => {
vi.mocked(fse.readdir).mockImplementation(async (p: fse.PathLike) => {
if (p === testWorkspaceFolder.uri.fsPath) {
return [new MockDirent(testLogicAppName, true)];
return [new MockDirent(testLogicAppName, true)] as any;
}
return [];
});
vi.spyOn(fse, 'pathExists').mockImplementation(async (p: fse.PathLike) => {
vi.mocked(fse.pathExists).mockImplementation(async (p: fse.PathLike) => {
return p === testWorkspaceFolder.uri.fsPath || p === testLogicAppChildFolder;
});
const isLogicAppProjectInRootSpy = vi
Expand Down Expand Up @@ -209,3 +221,114 @@ describe('ensureWorkspace', () => {
expect(result).toBe(true);
});
});

describe('createWorkspaceFile', () => {
let context: any;

beforeEach(() => {
context = {
telemetry: {
properties: {},
measurements: {},
},
};
vi.spyOn(funcCoreTools, 'addLocalFuncTelemetry').mockImplementation(() => {});
vi.mocked(fse.writeJson).mockResolvedValue(undefined);
vi.mocked(fse.copy).mockResolvedValue(undefined);
vi.mocked(fse.ensureDir).mockResolvedValue(undefined);
});

afterEach(() => {
vi.resetAllMocks();
});

it('in-place Logic App root: writes .code-workspace without copying', async () => {
const projectPath = path.resolve('/users/dev/MyLogicApp');
(vscode.workspace as any).workspaceFolders = [{ name: 'MyLogicApp', uri: { fsPath: projectPath } as vscode.Uri, index: 0 }];
vi.spyOn(verifyProject, 'isLogicAppProject').mockResolvedValue(true);
const executeCommandSpy = vi.spyOn(vscode.commands, 'executeCommand').mockResolvedValue(undefined);

await createWorkspaceFile(context, {
workspaceProjectPath: { fsPath: path.dirname(projectPath), path: path.dirname(projectPath) },
workspaceName: 'MyLogicApp',
});

expect(fse.copy).not.toHaveBeenCalled();
expect(fse.writeJson).toHaveBeenCalledWith(
path.join(projectPath, 'MyLogicApp.code-workspace'),
{ folders: [{ name: 'MyLogicApp', path: '.' }] },
{ spaces: 2 }
);
expect(executeCommandSpy).toHaveBeenCalledWith(
vscodeCommand.openFolder,
expect.objectContaining({ fsPath: path.join(projectPath, 'MyLogicApp.code-workspace') }),
true
);
});

it('in-place container with child directories: lists children without copying', async () => {
const containerPath = path.resolve('/users/dev/workspace-root');
(vscode.workspace as any).workspaceFolders = [{ name: 'workspace-root', uri: { fsPath: containerPath } as vscode.Uri, index: 0 }];
vi.spyOn(verifyProject, 'isLogicAppProject').mockResolvedValue(false);
vi.mocked(fse.readdir).mockResolvedValue([
new MockDirent('app1', true),
new MockDirent('app2', true),
new MockDirent('readme.md', false),
] as any);
const executeCommandSpy = vi.spyOn(vscode.commands, 'executeCommand').mockResolvedValue(undefined);

await createWorkspaceFile(context, {
workspaceProjectPath: { fsPath: path.dirname(containerPath), path: path.dirname(containerPath) },
workspaceName: 'workspace-root',
});

expect(fse.copy).not.toHaveBeenCalled();
expect(fse.writeJson).toHaveBeenCalledWith(
path.join(containerPath, 'workspace-root.code-workspace'),
{
folders: [
{ name: 'app1', path: './app1' },
{ name: 'app2', path: './app2' },
],
},
{ spaces: 2 }
);
expect(executeCommandSpy).toHaveBeenCalled();
});

it('external copy: copies project to new location', async () => {
const currentPath = path.resolve('/users/dev/MyLogicApp');
const externalParent = path.resolve('/users/workspaces');
const externalDest = path.join(externalParent, 'NewWorkspace');

(vscode.workspace as any).workspaceFolders = [{ name: 'MyLogicApp', uri: { fsPath: currentPath } as vscode.Uri, index: 0 }];
vi.spyOn(verifyProject, 'isLogicAppProject').mockResolvedValue(true);
const executeCommandSpy = vi.spyOn(vscode.commands, 'executeCommand').mockResolvedValue(undefined);

await createWorkspaceFile(context, {
workspaceProjectPath: { fsPath: externalParent, path: externalParent },
workspaceName: 'NewWorkspace',
});

expect(fse.ensureDir).toHaveBeenCalledWith(externalDest);
expect(fse.copy).toHaveBeenCalledWith(currentPath, path.join(externalDest, 'MyLogicApp'));
expect(fse.writeJson).toHaveBeenCalledWith(
path.join(externalDest, 'NewWorkspace.code-workspace'),
{ folders: [{ name: 'MyLogicApp', path: './MyLogicApp' }] },
{ spaces: 2 }
);
expect(executeCommandSpy).toHaveBeenCalled();
});

it('descendant rejection: throws when workspace target is inside current folder', async () => {
const currentPath = path.resolve('/users/dev/MyLogicApp');
(vscode.workspace as any).workspaceFolders = [{ name: 'MyLogicApp', uri: { fsPath: currentPath } as vscode.Uri, index: 0 }];

await expect(
createWorkspaceFile(context, {
workspaceProjectPath: { fsPath: currentPath, path: currentPath },
workspaceName: 'subdir',
})
).rejects.toThrow(/inside the currently open folder/);
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as vscode from 'vscode';
import {
pickCustomCodeNetHostProcessInternal,
pickCustomCodeWorkerChildProcess,
} from '../pickCustomCodeWorkerProcess';
import { pickCustomCodeNetHostProcessInternal, pickCustomCodeWorkerChildProcess } from '../pickCustomCodeWorkerProcess';
import * as validatePreDebug from '../../debug/validatePreDebug';
import { IRunningFuncTask, runningFuncTaskMap } from '../../utils/funcCoreTools/funcHostTask';
import * as pickFuncProcessModule from '../pickFuncProcess';
Expand All @@ -21,6 +18,14 @@ vi.mock('vscode', () => ({
},
}));

vi.mock('../../utils/delay', () => ({
delay: vi.fn((ms: number) => {
// Advance fake timers so the polling loop's Date.now() check terminates
vi.advanceTimersByTime(ms);
return Promise.resolve();
}),
}));

describe('pickCustomCodeNetHostProcessInternal', () => {
const testLogicAppName = 'LogicApp';
const testLogicAppPath = path.join('path', 'to', testLogicAppName);
Expand All @@ -31,15 +36,17 @@ describe('pickCustomCodeNetHostProcessInternal', () => {
name: testLogicAppName,
index: 0,
};
const testActionContext = {
telemetry: { properties: {} },
} as IActionContext;
let testActionContext: IActionContext;
const testFuncTask: IRunningFuncTask = {
startTime: Date.now(),
processId: Number(testFuncPid),
};

beforeEach(() => {
vi.useFakeTimers();
testActionContext = {
telemetry: { properties: {}, measurements: {} },
} as IActionContext;
(vscode.workspace as any).workspaceFolders = [testLogicAppWorkspaceFolder];

vi.spyOn(validatePreDebug, 'getMatchingWorkspaceFolder').mockReturnValue(testLogicAppWorkspaceFolder);
Expand All @@ -49,15 +56,17 @@ describe('pickCustomCodeNetHostProcessInternal', () => {
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
runningFuncTaskMap.clear();
});

it('should return undefined when no child dotnet process exists on the logic app functions host', async () => {
it('should throw when no child dotnet process exists on the logic app functions host after polling', async () => {
runningFuncTaskMap.set(testLogicAppWorkspaceFolder, testFuncTask);
await expect(
pickCustomCodeNetHostProcessInternal(testActionContext, testLogicAppWorkspaceFolder, testLogicAppPath)
).resolves.toBeUndefined();
await expect(pickCustomCodeNetHostProcessInternal(testActionContext, testLogicAppWorkspaceFolder, testLogicAppPath)).rejects.toThrow(
/Failed to find the .NET host child process/
);
expect(testActionContext.telemetry.properties.result).toBe('Failed');
expect(testActionContext.telemetry.properties.lastStep).toBe('pickNetHostChildProcess');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { defaultDependencyPathValue } from '../../../../constants';
import { ext } from '../../../../extensionVariables';
import { getDependencyTimeout, ensureRuntimeDependenciesDir } from '../../../utils/binaries';
import { ensureExtensionBundleHealthy, getDependenciesVersion } from '../../../utils/bundleFeed';
import { ensureExtensionBundleHealthy, getBundleDependencyFeed } from '../../../utils/bundleFeed';
import { recordDependencyUpdateCheck, shouldCheckForDependencyUpdates } from '../../../state/dependencies';
import { setDotNetCommand } from '../../../utils/dotnet/dotnet';
import { setFunctionsCommand } from '../../../utils/funcCoreTools/funcVersion';
Expand All @@ -28,7 +28,7 @@ vi.mock('../../../utils/binaries', () => ({
}));

vi.mock('../../../utils/bundleFeed', () => ({
getDependenciesVersion: vi.fn(),
getBundleDependencyFeed: vi.fn(),
ensureExtensionBundleHealthy: vi.fn().mockResolvedValue(undefined),
}));

Expand Down Expand Up @@ -106,7 +106,7 @@ describe('validateAndInstallBinaries', () => {
(shouldRequireStrictDependencyValidation as Mock).mockReturnValue(false);
(shouldCheckForDependencyUpdates as Mock).mockReturnValue(true);
(recordDependencyUpdateCheck as Mock).mockResolvedValue(undefined);
(getDependenciesVersion as Mock).mockResolvedValue({
(getBundleDependencyFeed as Mock).mockResolvedValue({
nodejs: '18.0.0',
funcCoreTools: '4.0.0',
dotnetVersions: '8.0.100',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { ensureRuntimeDependenciesDir, getDependencyTimeout } from '../../utils/binaries';
import { getDependenciesVersion, ensureExtensionBundleHealthy } from '../../utils/bundleFeed';
import { ensureExtensionBundleHealthy, getBundleDependencyFeed } from '../../utils/bundleFeed';
import { recordDependencyUpdateCheck, shouldCheckForDependencyUpdates } from '../../state/dependencies';
import { setDotNetCommand } from '../../utils/dotnet/dotnet';
import { setFunctionsCommand } from '../../utils/funcCoreTools/funcVersion';
Expand All @@ -17,7 +17,7 @@ import { validateDotNetIsLatest } from '../dotnet/validateDotNetIsLatest';
import { validateFuncCoreToolsIsLatest } from '../funcCoreTools/validateFuncCoreToolsIsLatest';
import { validateNodeJsIsLatest } from '../nodeJs/validateNodeJsIsLatest';
import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils';
import type { IBundleDependencyFeed } from '@microsoft/vscode-extension-logic-apps';
import type { IRuntimeDependencyVersions } from '@microsoft/vscode-extension-logic-apps';
import * as vscode from 'vscode';

export async function validateAndInstallBinaries(context: IActionContext) {
Expand Down Expand Up @@ -50,11 +50,11 @@ export async function validateAndInstallBinaries(context: IActionContext) {
const performedUpdateCheck = shouldCheckForDependencyUpdates();
context.telemetry.properties.performedDependencyUpdateCheck = `${performedUpdateCheck}`;

context.telemetry.properties.lastStep = 'getDependenciesVersion';
context.telemetry.properties.lastStep = 'getBundleDependencyFeed';
progress.report({ increment: 10, message: 'Get dependency version from CDN' });
let dependenciesVersions: IBundleDependencyFeed;
let dependenciesVersions: IRuntimeDependencyVersions;
try {
dependenciesVersions = await getDependenciesVersion(context);
dependenciesVersions = await getBundleDependencyFeed(context);
context.telemetry.properties.dependenciesVersions = JSON.stringify(dependenciesVersions);
} catch (error) {
// Unable to get dependency.json, will default to fallback versions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getWorkspaceRoot } from '../utils/workspace';
import { isCustomCodeFunctionsProject, tryGetLogicAppCustomCodeFunctionsProjects } from '../utils/customCodeUtils';
import * as vscode from 'vscode';
import { isNullOrUndefined } from '@microsoft/logic-apps-shared';
import { isPathEqual } from '../utils/fs';

/**
* Builds a custom code functions project if exists.
Expand Down Expand Up @@ -65,7 +66,7 @@ async function buildCustomCodeProject(functionsProjectPath: string): Promise<voi
const tasks: vscode.Task[] = await vscode.tasks.fetchTasks();
const buildTask = tasks.find((task) => {
const currTaskPath = (task.scope as vscode.WorkspaceFolder)?.uri.fsPath;
return task.name === 'build' && currTaskPath === functionsProjectPath;
return task.name === 'build' && !!currTaskPath && isPathEqual(currTaskPath, functionsProjectPath);
});

if (!buildTask) {
Expand All @@ -75,7 +76,8 @@ async function buildCustomCodeProject(functionsProjectPath: string): Promise<voi
return new Promise<void>((resolve, reject) => {
const disposable: vscode.Disposable = vscode.tasks.onDidEndTaskProcess((e) => {
const isMatchingTask =
(e.execution.task.scope as vscode.WorkspaceFolder)?.uri.fsPath === functionsProjectPath && e.execution.task.name === buildTask.name;
isPathEqual((e.execution.task.scope as vscode.WorkspaceFolder)?.uri.fsPath ?? '', functionsProjectPath) &&
e.execution.task.name === buildTask.name;

if (isMatchingTask) {
disposable.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,5 @@ export async function createCustomCodeFunction(context: IActionContext, folderPa
context.telemetry.properties.errorMessage = err.message;
throw err;
}
context.telemetry.properties.result = 'Succeeded';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ export async function loadDataMapFile(context: IActionContext, uri: Uri): Promis
if (fileUris && fileUris.length > 0) {
// Copy the schema file they selected to the Schemas folder (can safely continue map definition loading)
await fs.copyFile(fileUris[0].fsPath, schemaPath);
context.telemetry.properties.result = 'Succeeded';

return true;
}
}
Expand Down
2 changes: 0 additions & 2 deletions apps/vs-code-designer/src/app/commands/debugLogicApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { localize } from '../../localize';
import { ext } from '../../extensionVariables';
import { tryGetLogicAppProjectRoot } from '../utils/verifyIsProject';
import { pickCustomCodeNetFxWorkerProcessInternal, pickCustomCodeNetHostProcessInternal } from './pickCustomCodeWorkerProcess';
import { extensionCommand } from '../../constants';

export async function debugLogicApp(
context: IActionContext,
Expand Down Expand Up @@ -142,5 +141,4 @@ export async function debugLogicApp(
)
);
}
context.telemetry.properties.result = 'Succeeded';
}
10 changes: 5 additions & 5 deletions apps/vs-code-designer/src/app/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,16 @@ async function deploy(
addLocalFuncTelemetry(context);

let deployProjectPathForWorkflowApp: string | undefined;
const settingsToExclude: string[] = [
const settingsToExclude: (RegExp | string)[] = [
webhookRedirectHostUri,
azureWebJobsStorageKey,
ProjectDirectoryPathKey,
workflowAuthenticationMethodKey,
'REMOTEDEBUGGINGVERSION',
// Local-debug-only: absolute path to the local Node.js binary used by the inline-code
// language worker. Deploying it points the cloud worker at a non-existent local path,
// which hangs "Execute JavaScript Code" actions in a Running state indefinitely.
inlineCodeNodeExecutablePathKey,
/^WEBSITE_/,
/^FUNCTIONS_RUNTIME/,
'ScmType',
];

context.telemetry.properties.lastStep = 'getDeployFsPath';
Expand Down Expand Up @@ -410,7 +410,7 @@ async function managedApiConnectionsExists(workspaceFolder: WorkspaceFolder): Pr
async function getProjectPathToDeploy(
node: SlotTreeItem,
workspaceFolder: WorkspaceFolder,
settingsToExclude: string[],
settingsToExclude: (RegExp | string)[],
originalDeployFsPath: string,
identityWizardContext: IIdentityWizardContext,
actionContext: IActionContext
Expand Down
Loading
Loading