Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .github/workflows/pr-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,13 @@ jobs:
apps/vs-code-designer/src/app/commands/deploy/deploy.ts
apps/vs-code-designer/src/app/commands/workflows/exportLogicApp.ts
apps/vs-code-designer/src/app/utils/startRuntimeApi.ts
apps/vs-code-designer/src/app/utils/codeless/startDesignTimeApi.ts
apps/vs-code-designer/src/app/utils/binaries.ts
apps/vs-code-designer/src/extensionVariables.ts
apps/vs-code-designer/src/main.ts
# VS Code connector/runtime orchestration glue; focused behavior is
# covered by command/helper unit tests and VS Code E2E scenarios.
apps/vs-code-designer/src/app/utils/codeless/common.ts
# VS Code project-scaffolding files (depend on VS Code APIs; tested via E2E).
# The pure-function PATH helper `funcHostTaskEnv.ts` has its own unit
# tests in `src/app/utils/codeless/__test__/funcHostTaskEnv.test.ts`;
Expand Down
112 changes: 110 additions & 2 deletions .github/workflows/vscode-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,117 @@ jobs:
if-no-files-found: ignore
retention-days: 30

vscode-e2e-compat:
name: vscode-e2e compatibility (${{ matrix.label }})
needs: setup-extension-build
timeout-minutes: 20
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# The runner Node version drives pnpm/tsup/run-e2e.js and any child
# processes that resolve node from PATH. The extension-host Node
# version is owned by the VS Code/Electron build below.
#
# Keep an explicit legacy smoke so the latest extension continues to
# activate on the stable runner/runtime combination used by the full
# VS Code E2E suite.
- label: node20-vscode108
node-version: 20.x
vscode-version: 1.108.0
- label: node22-vscode108
node-version: 22.x
vscode-version: 1.108.0
# VS Code 1.123 carries the Electron/Node 24 runtime that exposed the
# removed node:util helper regression fixed by this branch.
- label: node24-vscode123
node-version: 24.x
vscode-version: 1.123.0
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Symlink node to system path
run: |
NODE_BIN="$(which node)"
NPM_BIN="$(which npm)"
NPX_BIN="$(which npx)"
for dir in /usr/local/bin /usr/bin; do
sudo ln -sf "$NODE_BIN" "$dir/node"
sudo ln -sf "$NPM_BIN" "$dir/npm"
sudo ln -sf "$NPX_BIN" "$dir/npx"
done
echo "Symlinked node $(node --version) into /usr/local/bin and /usr/bin"
env -i /usr/bin/node --version

- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.local/share/pnpm/store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9.1.3
run_install: |
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]

- name: Download extension build artifact
uses: actions/download-artifact@v4
with:
name: extension-build-${{ github.sha }}
path: .

- name: Extract build artifacts
run: tar -xzf extension-build.tar.gz

- name: Install system dependencies for virtual display
run: |
sudo apt-get update
sudo apt-get install -y xvfb libgbm-dev libgtk-3-0 libnss3 libasound2t64 libxss1 libatk-bridge2.0-0 libatk1.0-0

- name: Run compatibility activation smoke
run: |
export PATH="$(dirname $(which node)):/usr/local/bin:/usr/bin:/bin:$PATH"
echo "PATH=$PATH"
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
node apps/vs-code-designer/src/test/ui/run-e2e.js
env:
LA_E2E_SCENARIO: p40-nonlogicapp
E2E_VSCODE_VERSION: ${{ matrix.vscode-version }}
NODE_OPTIONS: --max-old-space-size=4096
TEMP: ${{ runner.temp }}
TMP: ${{ runner.temp }}
TMPDIR: ${{ runner.temp }}

- name: Upload test screenshots (always)
uses: actions/upload-artifact@v4
if: always()
with:
name: vscode-e2e-screenshots-compat-${{ matrix.label }}
path: |
${{ runner.temp }}/test-resources/screenshots/
test-resources/screenshots/
if-no-files-found: ignore
retention-days: 30

# Rollup status check so branch protection can require a single check name
# ("vscode-e2e-summary") regardless of how many scenarios we add later.
vscode-e2e-summary:
name: vscode-e2e-summary
needs: [setup-extension-build, setup-fixtures, vscode-e2e]
needs: [setup-extension-build, setup-fixtures, vscode-e2e, vscode-e2e-compat]
if: always()
runs-on: ubuntu-latest
steps:
Expand All @@ -429,9 +535,11 @@ jobs:
echo "setup-extension-build: ${{ needs.setup-extension-build.result }}"
echo "setup-fixtures: ${{ needs.setup-fixtures.result }}"
echo "vscode-e2e (matrix): ${{ needs.vscode-e2e.result }}"
echo "compat matrix: ${{ needs.vscode-e2e-compat.result }}"
if [ "${{ needs.setup-extension-build.result }}" != "success" ] || \
[ "${{ needs.setup-fixtures.result }}" != "success" ] || \
[ "${{ needs.vscode-e2e.result }}" != "success" ]; then
[ "${{ needs.vscode-e2e.result }}" != "success" ] || \
[ "${{ needs.vscode-e2e-compat.result }}" != "success" ]; then
echo "::error::One or more vscode-e2e jobs failed"
exit 1
fi
Expand Down
49 changes: 49 additions & 0 deletions apps/vs-code-designer/src/__test__/nodeUtilCompatibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import util from 'node:util';

type LegacyNodeUtil = {
isArray?: (value: unknown) => value is unknown[];
isNullOrUndefined?: (value: unknown) => value is null | undefined;
isNumber?: (value: unknown) => value is number;
};

const legacyUtil = util as LegacyNodeUtil;
const originalHelpers = {
isArray: legacyUtil.isArray,
isNullOrUndefined: legacyUtil.isNullOrUndefined,
isNumber: legacyUtil.isNumber,
};

function restoreHelper<T extends keyof LegacyNodeUtil>(name: T, value: LegacyNodeUtil[T]): void {
if (value === undefined) {
delete legacyUtil[name];
} else {
legacyUtil[name] = value;
}
}

describe('applyNodeUtilCompatibility', () => {
afterEach(() => {
restoreHelper('isArray', originalHelpers.isArray);
restoreHelper('isNullOrUndefined', originalHelpers.isNullOrUndefined);
restoreHelper('isNumber', originalHelpers.isNumber);
vi.resetModules();
});

it('restores legacy util helpers removed from newer Node runtimes', async () => {
legacyUtil.isArray = undefined;
legacyUtil.isNullOrUndefined = undefined;
legacyUtil.isNumber = undefined;

const { applyNodeUtilCompatibility } = await import('../nodeUtilCompatibility');
applyNodeUtilCompatibility();

expect(legacyUtil.isArray?.([])).toBe(true);
expect(legacyUtil.isArray?.('value')).toBe(false);
expect(legacyUtil.isNullOrUndefined?.(null)).toBe(true);
expect(legacyUtil.isNullOrUndefined?.(undefined)).toBe(true);
expect(legacyUtil.isNullOrUndefined?.('value')).toBe(false);
expect(legacyUtil.isNumber?.(1)).toBe(true);
expect(legacyUtil.isNumber?.('1')).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PackageManager, dotnetDependencyName, funcDependencyName, nodeJsDependencyName } from '../../../constants';
import { downloadAndExtractDependency } from '../../utils/binaries';
import { executeCommand } from '../../utils/funcCoreTools/cpUtils';
import { getNpmDistTag } from '../../utils/funcCoreTools/getNpmDistTag';
import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath';
import { promptForFuncVersion } from '../../utils/vsCodeConfig/settings';
import { installDotNet } from '../dotnet/installDotNet';
import { installFuncCoreToolsBinaries, installFuncCoreToolsSystem } from '../funcCoreTools/installFuncCoreTools';
import { installNodeJs } from '../nodeJs/installNodeJs';

vi.mock('../../../extensionVariables', () => ({
ext: {
outputChannel: {
show: vi.fn(),
appendLog: vi.fn(),
},
},
}));

vi.mock('../../utils/runtimeDependenciesPath', () => ({
ensureRuntimeDependenciesPath: vi.fn(),
}));

vi.mock('../../utils/binaries', () => ({
downloadAndExtractDependency: vi.fn(),
getCpuArchitecture: vi.fn(() => 'x64'),
getDotNetBinariesReleaseUrl: vi.fn(() => 'https://dot.net/v1/dotnet-install.ps1'),
getFunctionCoreToolsBinariesReleaseUrl: vi.fn(() => 'https://example.com/func.zip'),
getLatestFunctionCoreToolsVersion: vi.fn(async () => '4.0.0'),
getLatestNodeJsVersion: vi.fn(async () => '20.0.0'),
getNodeJsBinariesReleaseUrl: vi.fn(() => 'https://example.com/node.zip'),
}));

vi.mock('../../utils/funcCoreTools/cpUtils', () => ({
executeCommand: vi.fn(),
}));

vi.mock('../../utils/funcCoreTools/getBrewPackageName', () => ({
getBrewPackageName: vi.fn(() => 'azure-functions-core-tools@4'),
}));

vi.mock('../../utils/funcCoreTools/getNpmDistTag', () => ({
getNpmDistTag: vi.fn(),
}));

vi.mock('../../utils/vsCodeConfig/settings', () => ({
promptForFuncVersion: vi.fn(),
}));

vi.mock('vscode-nls', () => ({
localize: (_key: string, message: string, ...args: unknown[]) =>
message.replace(/{(\d+)}/g, (_match, index) => String(args[Number(index)] ?? '')),
}));

describe('runtime dependency installers', () => {
const context = { telemetry: { properties: {} } } as any;
const originalPlatform = process.platform;

beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(process, 'platform', { value: originalPlatform });
vi.mocked(ensureRuntimeDependenciesPath).mockResolvedValue('D:\\dependencies');
vi.mocked(downloadAndExtractDependency).mockResolvedValue(undefined);
vi.mocked(getNpmDistTag).mockResolvedValue({ tag: '4.0.0' } as any);
vi.mocked(promptForFuncVersion).mockResolvedValue('4' as any);
});

it('installs .NET into the ensured runtime dependency path', async () => {
await installDotNet(context, '8');

expect(ensureRuntimeDependenciesPath).toHaveBeenCalled();
expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://dot.net/v1/dotnet-install.ps1',
'D:\\dependencies',
dotnetDependencyName,
null,
'8'
);
});

it('installs Functions Core Tools binaries into the ensured runtime dependency path', async () => {
await installFuncCoreToolsBinaries(context, '4');

expect(ensureRuntimeDependenciesPath).toHaveBeenCalled();
expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://example.com/func.zip',
'D:\\dependencies',
funcDependencyName
);
});

it('uses the macOS Functions Core Tools binary release when running on macOS', async () => {
Object.defineProperty(process, 'platform', { value: 'darwin' });

await installFuncCoreToolsBinaries(context, '4');

expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://example.com/func.zip',
'D:\\dependencies',
funcDependencyName
);
});

it('installs Node.js into the ensured runtime dependency path', async () => {
await installNodeJs(context, '20');

expect(ensureRuntimeDependenciesPath).toHaveBeenCalled();
expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://example.com/node.zip',
'D:\\dependencies',
nodeJsDependencyName
);
});

it('uses the Linux Node.js binary release when running on Linux', async () => {
Object.defineProperty(process, 'platform', { value: 'linux' });

await installNodeJs(context, '20');

expect(downloadAndExtractDependency).toHaveBeenCalledWith(
context,
'https://example.com/node.zip',
'D:\\dependencies',
nodeJsDependencyName
);
});

it('installs Functions Core Tools through npm when system installation is selected', async () => {
await installFuncCoreToolsSystem(context, [PackageManager.npm], '4' as any);

expect(executeCommand).toHaveBeenCalledWith(expect.anything(), undefined, 'npm', 'install', '-g', 'azure-functions-core-tools@4.0.0');
});

it('installs Functions Core Tools through brew when that package manager is selected', async () => {
await installFuncCoreToolsSystem(context, [PackageManager.brew], '4' as any);

expect(executeCommand).toHaveBeenCalledWith(expect.anything(), undefined, 'brew', 'tap', 'azure/functions');
expect(executeCommand).toHaveBeenCalledWith(expect.anything(), undefined, 'brew', 'install', 'azure-functions-core-tools@4');
});

it('rejects unsupported system package managers', async () => {
await expect(installFuncCoreToolsSystem(context, ['unknown' as PackageManager], '4' as any)).rejects.toThrow('Invalid package manager');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { autoRuntimeDependenciesPathSettingKey, defaultDependencyPathValue } from '../../../constants';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { getDependencyTimeout } from '../../utils/binaries';
Expand All @@ -12,9 +11,9 @@ import { executeCommand } from '../../utils/funcCoreTools/cpUtils';
import { setFunctionsCommand } from '../../utils/funcCoreTools/funcVersion';
import { installLSPSDK } from '../../utils/languageServerProtocol';
import { setNodeJsCommand } from '../../utils/nodeJs/nodeJsVersion';
import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath';
import { runWithDurationTelemetry } from '../../utils/telemetry';
import { timeout } from '../../utils/timeout';
import { getGlobalSetting, updateGlobalSetting } from '../../utils/vsCodeConfig/settings';
import { validateDotNetIsLatest } from '../dotnet/validateDotNetIsLatest';
import { validateFuncCoreToolsIsLatest } from '../funcCoreTools/validateFuncCoreToolsIsLatest';
import { validateNodeJsIsLatest } from '../nodeJs/validateNodeJsIsLatest';
Expand Down Expand Up @@ -43,10 +42,7 @@ export async function validateAndInstallBinaries(context: IActionContext) {
const dependencyTimeout = getDependencyTimeout() * 1000;

context.telemetry.properties.dependencyTimeout = `${dependencyTimeout} milliseconds`;
if (!getGlobalSetting<string>(autoRuntimeDependenciesPathSettingKey)) {
await updateGlobalSetting(autoRuntimeDependenciesPathSettingKey, defaultDependencyPathValue);
context.telemetry.properties.dependencyPath = defaultDependencyPathValue;
}
context.telemetry.properties.dependencyPath = await ensureRuntimeDependenciesPath();

context.telemetry.properties.lastStep = 'getDependenciesVersion';
progress.report({ increment: 10, message: 'Get dependency version from CDN' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { autoRuntimeDependenciesPathSettingKey, dotnetDependencyName } from '../../../constants';
import { dotnetDependencyName } from '../../../constants';
import { ext } from '../../../extensionVariables';
import { downloadAndExtractDependency, getDotNetBinariesReleaseUrl } from '../../utils/binaries';
import { getGlobalSetting } from '../../utils/vsCodeConfig/settings';
import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath';
import type { IActionContext } from '@microsoft/vscode-azext-utils';

export async function installDotNet(context: IActionContext, majorVersion?: string): Promise<void> {
ext.outputChannel.show();
context.telemetry.properties.majorVersion = majorVersion;
const targetDirectory = getGlobalSetting<string>(autoRuntimeDependenciesPathSettingKey);
const targetDirectory = await ensureRuntimeDependenciesPath();

context.telemetry.properties.lastStep = 'getDotNetBinariesReleaseUrl';
const scriptUrl = getDotNetBinariesReleaseUrl();
Expand Down
Loading
Loading