Skip to content
Open
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
116 changes: 115 additions & 1 deletion .github/workflows/vscode-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,118 @@ jobs:
if-no-files-found: ignore
retention-days: 30

vscode-e2e-customcode-dotnet:
name: vscode-e2e-customcode-dotnet (ubuntu-latest, ${{ matrix.version }})
needs: setup-extension-build
# The net8 leg creates a real workspace and proves the full debug/run
# lifecycle. The net10 leg opens the real wizard and verifies the option is
# hidden. 60 min leaves room for one full net8 retry and a cold restore.
timeout-minutes: 60
runs-on: ubuntu-latest
# Sharded by dotnet target rather than looping both in one job, same
# reasoning as vscode-e2e-azurite's app-kind sharding: keeps wall-clock at
# the single-target baseline and names the target that broke. Matrix legs
# roll up into a single `needs.<job>.result`, so the summary gate needs no
# extra entries.
strategy:
fail-fast: false
matrix:
version: [net8, net10]
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 20.x
package-manager-cache: false

- name: Symlink node to system path (for func host child processes)
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

# The net8 leg creates, debugs, and runs a custom-code project. The net10
# leg only verifies that the option is hidden from the real wizard.
- name: Setup .NET SDK 8
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.x

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

- name: Download extension build artifact
uses: actions/download-artifact@v7
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: Restore Logic Apps runtime dependencies
id: la-deps-cache
uses: actions/cache/restore@v5
with:
path: ~/.azurelogicapps/dependencies
key: la-runtime-deps-${{ runner.os }}-v1
restore-keys: |
la-runtime-deps-${{ runner.os }}-

- name: Report runtime dependency cache state
run: |
echo "cache-hit: ${{ steps.la-deps-cache.outputs.cache-hit }}"
echo "cache-matched-key: ${{ steps.la-deps-cache.outputs.cache-matched-key }}"

- name: Run custom-code .NET version create/assert + debug lifecycle E2E
run: |
export CUSTOMCODE_DOTNET_BINARY_PATH="$(command -v dotnet)"
export CUSTOMCODE_DOTNET_ROOT="$(dirname "$CUSTOMCODE_DOTNET_BINARY_PATH")"
"$CUSTOMCODE_DOTNET_BINARY_PATH" --list-sdks
export PATH="$(dirname $(which node)):/usr/local/bin:/usr/bin:/bin:$PATH"
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
node apps/vs-code-designer/out/test/run-e2e.js
env:
E2E_MODE: customcodedotnetonly
CUSTOMCODE_DOTNET_E2E_VERSIONS: ${{ matrix.version }}
LA_E2E_SCENARIO_RETRIES: '1'
NODE_OPTIONS: --max-old-space-size=4096
TEMP: ${{ runner.temp }}
TMPDIR: ${{ runner.temp }}

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

# ---------------------------------------------------------------------------
# Windows runtime-dependency bootstrap.
#
Expand Down Expand Up @@ -1855,7 +1967,7 @@ jobs:
# ("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, vscode-e2e-windows, vscode-e2e-codeful-ubuntu, vscode-e2e-codeful-windows, vscode-e2e-azurite, vscode-e2e-azurite-windows, vscode-e2e-funcselfheal-windows, vscode-e2e-compat, setup-runtime-deps-windows]
needs: [setup-extension-build, setup-fixtures, vscode-e2e, vscode-e2e-windows, vscode-e2e-codeful-ubuntu, vscode-e2e-codeful-windows, vscode-e2e-azurite, vscode-e2e-azurite-windows, vscode-e2e-funcselfheal-windows, vscode-e2e-customcode-dotnet, vscode-e2e-compat, setup-runtime-deps-windows]
if: always()
runs-on: ubuntu-latest
steps:
Expand All @@ -1875,6 +1987,7 @@ jobs:
echo "vscode-e2e-azurite: ${{ needs.vscode-e2e-azurite.result }}"
echo "vscode-e2e-azurite-windows: ${{ needs.vscode-e2e-azurite-windows.result }}"
echo "vscode-e2e-funcselfheal-windows: ${{ needs.vscode-e2e-funcselfheal-windows.result }}"
echo "vscode-e2e-customcode-dotnet: ${{ needs.vscode-e2e-customcode-dotnet.result }}"
echo "setup-runtime-deps-windows: ${{ needs.setup-runtime-deps-windows.result }} (advisory, not gated: seeds la-runtime-deps-Windows-v1)"
echo "compat matrix: ${{ needs.vscode-e2e-compat.result }}"
if [ "${{ needs.setup-extension-build.result }}" != "success" ] || \
Expand All @@ -1886,6 +1999,7 @@ jobs:
[ "${{ needs.vscode-e2e-azurite.result }}" != "success" ] || \
[ "${{ needs.vscode-e2e-azurite-windows.result }}" != "success" ] || \
[ "${{ needs.vscode-e2e-funcselfheal-windows.result }}" != "success" ] || \
[ "${{ needs.vscode-e2e-customcode-dotnet.result }}" != "success" ] || \
[ "${{ needs.vscode-e2e-compat.result }}" != "success" ]; then
echo "::error::One or more required vscode-e2e jobs failed"
exit 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from './CreateLogicAppWorkspace';
import { devContainerFolderName, devContainerFileName } from '../../../../constants';
import { ext } from '../../../../extensionVariables';
import { addCustomCodeDotNetVersionSetting } from '../../../utils/appSettings/localSettings';

export async function createLogicAppProject(context: IActionContext, options: any, workspaceRootFolder: any): Promise<void> {
addLocalFuncTelemetry(context);
Expand Down Expand Up @@ -91,6 +92,12 @@ export async function createLogicAppProject(context: IActionContext, options: an
const createFunctionAppFilesStep = new CreateFunctionAppFiles();
await createFunctionAppFilesStep.setup(mySubContext);
}

// Records the .NET version used by the associated custom-code project (net8/net10.0) in the
// Logic App's own local.settings.json, at context.projectPath, the authoritative Logic App project
// directory. No-op for rulesEngine, codeful, standard, or NetFx.
await addCustomCodeDotNetVersionSetting(context, mySubContext.projectPath, mySubContext.projectType, mySubContext.targetFramework);

ext.outputChannel.appendLog(localize('finishedCreating', 'Finished creating project.'));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { ProjectType, WorkflowType } from '@microsoft/vscode-extension-logic-app
import { createDevContainerContents, createLogicAppVsCodeContents } from './CreateLogicAppVSCodeContents';
import { logicAppPackageProcessing, unzipLogicAppPackageIntoWorkspace } from '../../../utils/cloudToLocalUtils';
import { getGlobalSetting } from '../../../utils/vsCodeConfig/settings';
import { addCustomCodeDotNetVersionSetting } from '../../../utils/appSettings/localSettings';

export async function createRulesFiles(context: IFunctionWizardContext): Promise<void> {
if (context.projectType === ProjectType.rulesEngine) {
Expand Down Expand Up @@ -356,6 +357,12 @@ export async function createLogicAppWorkspace(context: IActionContext, options:
const createFunctionAppFilesStep = new CreateFunctionAppFiles();
await createFunctionAppFilesStep.setup(mySubContext);
}

// Records the .NET version used by the associated custom-code project (net8/net10.0) in the
// Logic App's own local.settings.json, at context.projectPath, the authoritative Logic App project
// directory. No-op for rulesEngine, codeful, standard, or NetFx.
await addCustomCodeDotNetVersionSetting(context, mySubContext.projectPath, mySubContext.projectType, mySubContext.targetFramework);

ext.outputChannel.appendLog(localize('finishedCreating', 'Finished creating project.'));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,15 @@ describe('createLogicAppProject', () => {
setup: mockSetup,
}));

// Simulate the Logic App's local.settings.json already existing on disk (as it would after
// the real createLocalConfigurationFiles ran), so the LOGIC_APPS_CUSTOMCODE_DOTNETVERSION
// write merges into it instead of falling back to full settings generation.
await fse.ensureDir(logicAppFolderPath);
await fse.writeJson(path.join(logicAppFolderPath, 'local.settings.json'), {
IsEncrypted: false,
Values: { SomeExistingSetting: 'keepMe' },
});

await createLogicAppProject(mockContext, customCodeOptions, workspaceRootFolder);

expect(mockSetup).toHaveBeenCalledWith(
Expand All @@ -381,6 +390,12 @@ describe('createLogicAppProject', () => {
targetFramework: 'net8',
})
);

// Verify the setting is written to the authoritative Logic App project path (context.projectPath),
// not a derived function-project path, and that unrelated existing settings are preserved.
const localSettings = await fse.readJson(path.join(logicAppFolderPath, 'local.settings.json'));
expect(localSettings.Values.LOGIC_APPS_CUSTOMCODE_DOTNETVERSION).toBe('net8');
expect(localSettings.Values.SomeExistingSetting).toBe('keepMe');
});

it('should create custom code project with Net10 target framework', async () => {
Expand All @@ -395,6 +410,12 @@ describe('createLogicAppProject', () => {
setup: mockSetup,
}));

await fse.ensureDir(logicAppFolderPath);
await fse.writeJson(path.join(logicAppFolderPath, 'local.settings.json'), {
IsEncrypted: false,
Values: { SomeExistingSetting: 'keepMe' },
});

await createLogicAppProject(mockContext, customCodeOptions, workspaceRootFolder);

expect(mockSetup).toHaveBeenCalledWith(
Expand All @@ -403,6 +424,35 @@ describe('createLogicAppProject', () => {
targetFramework: 'net10.0',
})
);

const localSettings = await fse.readJson(path.join(logicAppFolderPath, 'local.settings.json'));
expect(localSettings.Values.LOGIC_APPS_CUSTOMCODE_DOTNETVERSION).toBe('net10.0');
expect(localSettings.Values.SomeExistingSetting).toBe('keepMe');
});

it('should not add the LOGIC_APPS_CUSTOMCODE_DOTNETVERSION setting for NetFx target framework', async () => {
const customCodeOptions = {
...mockOptions,
logicAppType: ProjectType.customCode,
targetFramework: 'net472',
};

const mockSetup = vi.fn().mockResolvedValue(undefined);
(CreateFunctionAppFiles as Mock).mockImplementation(() => ({
setup: mockSetup,
}));

await fse.ensureDir(logicAppFolderPath);
await fse.writeJson(path.join(logicAppFolderPath, 'local.settings.json'), {
IsEncrypted: false,
Values: { SomeExistingSetting: 'keepMe' },
});

await createLogicAppProject(mockContext, customCodeOptions, workspaceRootFolder);

const localSettings = await fse.readJson(path.join(logicAppFolderPath, 'local.settings.json'));
expect(localSettings.Values.LOGIC_APPS_CUSTOMCODE_DOTNETVERSION).toBeUndefined();
expect(localSettings.Values.SomeExistingSetting).toBe('keepMe');
});

it('should pass correct function parameters to custom code project', async () => {
Expand Down Expand Up @@ -1387,6 +1437,63 @@ local.settings.json`
});
});

describe('Custom Code DotNet Version Local Setting', () => {
const runCreateLogicAppProject = async (targetFramework: string) => {
const options: IWebviewProjectContext = {
workspaceProjectPath: { fsPath: tempDir } as vscode.Uri,
workspaceName: 'TestWorkspace',
logicAppName: 'TestLogicApp',
logicAppType: ProjectType.customCode,
workflowName: 'MyWorkflow',
workflowType: 'Stateful',
functionFolderName: 'Functions',
functionName: 'MyFunction',
functionNamespace: 'MyNamespace',
targetFramework,
} as any;

const functionAppFiles = createTestFunctionAppFiles();
vi.mocked(CreateFunctionAppFiles).mockImplementation(
() =>
({
setup: (ctx: IProjectWizardContext) => functionAppFiles.setup(ctx),
hideStepCount: true,
}) as any
);

await createLogicAppProject(mockContext, options, workspaceRootFolder);

return fse.readJson(path.join(logicAppFolderPath, 'local.settings.json'));
};

it('should write LOGIC_APPS_CUSTOMCODE_DOTNETVERSION="net8" to the Logic App root local.settings.json for Net8', async () => {
const localSettings = await runCreateLogicAppProject('net8');

// Authoritative path: written to the Logic App's own folder, not the Functions sub-folder.
const functionsLocalSettingsPath = path.join(workspaceRootFolder, 'Functions', 'local.settings.json');
expect(await fse.pathExists(functionsLocalSettingsPath)).toBe(false);

expect(localSettings.Values.LOGIC_APPS_CUSTOMCODE_DOTNETVERSION).toBe('net8');
// Unrelated settings written by createLocalConfigurationFiles must be preserved.
expect(localSettings.Values.AzureWebJobsStorage).toBe('UseDevelopmentStorage=true');
expect(localSettings.Values.FUNCTIONS_WORKER_RUNTIME).toBe('node');
});

it('should write LOGIC_APPS_CUSTOMCODE_DOTNETVERSION="net10.0" to the Logic App root local.settings.json for Net10', async () => {
const localSettings = await runCreateLogicAppProject('net10.0');

expect(localSettings.Values.LOGIC_APPS_CUSTOMCODE_DOTNETVERSION).toBe('net10.0');
expect(localSettings.Values.AzureWebJobsStorage).toBe('UseDevelopmentStorage=true');
expect(localSettings.Values.FUNCTIONS_WORKER_RUNTIME).toBe('node');
});

it('should not write LOGIC_APPS_CUSTOMCODE_DOTNETVERSION for NetFx custom code projects', async () => {
const localSettings = await runCreateLogicAppProject('net472');

expect(localSettings.Values.LOGIC_APPS_CUSTOMCODE_DOTNETVERSION).toBeUndefined();
});
});

describe('Rules Engine Project Integration', () => {
it('should create rules folder structure', async () => {
const options: IWebviewProjectContext = {
Expand Down Expand Up @@ -1454,6 +1561,10 @@ local.settings.json`
const csContent = await fse.readFile(csFilePath, 'utf-8');
expect(csContent).toContain('namespace Rules.Namespace');
expect(csContent).toContain('class RulesFunction');

// rulesEngine projects must never receive the customCode-only setting, even with Net8.
const localSettings = await fse.readJson(path.join(logicAppFolderPath, 'local.settings.json'));
expect(localSettings.Values.LOGIC_APPS_CUSTOMCODE_DOTNETVERSION).toBeUndefined();
});

it('should create ContosoPurchase.cs file for rules engine', async () => {
Expand Down
Loading