Skip to content

Add support for calling kiota via standard npx command #6453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
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
16 changes: 10 additions & 6 deletions .github/workflows/build-vscode-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
id: checksecret_job
run: |
echo "is_SONAR_TOKEN_set=${{ env.SONAR_TOKEN != '' }}" >> $GITHUB_OUTPUT

test_and_generate_binaries:
needs: [checksecret]
strategy:
Expand Down Expand Up @@ -48,7 +48,7 @@ jobs:
- name: Use .NET 9
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.x'
dotnet-version: "9.x"

- id: last_release
run: |
Expand All @@ -61,8 +61,10 @@ jobs:

- run: scripts/update-vscode-releases.ps1 -version "v${{ steps.last_release.outputs.RELEASE_VERSION }}" -packageJsonFilePath "./vscode/microsoft-kiota/package.json" -runtimeFilePath "./vscode/npm-package/runtime.json" -online
shell: pwsh

- name: Install dependencies
env:
SKIP_POSTINSTALL: "true"
run: npm install
working-directory: vscode

Expand Down Expand Up @@ -145,19 +147,21 @@ jobs:
shell: pwsh

- name: Install dependencies
env:
SKIP_POSTINSTALL: "true"
run: npm install
working-directory: vscode

- run: npm run package:${{ matrix.builds.id }}
if: matrix.builds.id == 'package'
name: 'Package - ${{ matrix.builds.id }}'
name: "Package - ${{ matrix.builds.id }}"
working-directory: vscode

- run: |
npm i -g @vscode/vsce
vsce package
working-directory: vscode/${{ matrix.builds.path }}
name: 'Package - ${{ matrix.builds.id }}'
name: "Package - ${{ matrix.builds.id }}"
if: matrix.builds.id == 'vscode'

- name: Upload artifact
Expand All @@ -176,4 +180,4 @@ jobs:
run: exit 0
- name: One or more build matrix options failed
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
run: exit 1
2 changes: 2 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ jobs:
- name: Restore dependencies
run: dotnet restore
- name: Build
env:
SKIP_POSTINSTALL: "true"
run: dotnet build --no-restore -c Release

- name: Perform CodeQL Analysis
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
- name: Check formatting
run: dotnet format --verify-no-changes
- name: Build
env:
SKIP_POSTINSTALL: "true"
run: dotnet build kiota.sln --no-restore
- name: Test
run: dotnet test kiota.sln --no-build --verbosity normal --collect:"XPlat Code Coverage"
Expand Down
49 changes: 44 additions & 5 deletions kiota.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30804.86
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kiota", "src\kiota\kiota.csproj", "{944FCE5E-0CFA-4018-B353-E14FA1395007}"
EndProject
Expand All @@ -19,6 +19,28 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EAAC5CEA-33B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KiotaGenerated", "src\Kiota.Generated\KiotaGenerated.csproj", "{01ABDF23-60CD-4CE3-8DC7-8654C4BA1EE8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kiota.NPM.IntegrationTests", "tests\Kiota.NPM.IntegrationTests\Kiota.NPM.IntegrationTests.csproj", "{51296EA6-7A4A-2A93-5060-1FBD4CB31CC5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NPMProjects", "NPMProjects", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "npm-package", "vscode\npm-package\npm-package.esproj", "{C6BFB632-723D-238A-EA11-44E78644612E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{099BEFDC-D101-4DB6-B028-E24376B6DC50}"
ProjectSection(SolutionItems) = preProject
.github\workflows\auto-merge-dependabot.yml = .github\workflows\auto-merge-dependabot.yml
.github\workflows\build-vscode-extension.yml = .github\workflows\build-vscode-extension.yml
.github\workflows\check-translations.yml = .github\workflows\check-translations.yml
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\workflows\codeql-required-workaround.yml = .github\workflows\codeql-required-workaround.yml
.github\workflows\dotnet-required-workaround.yml = .github\workflows\dotnet-required-workaround.yml
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
.github\workflows\idempotency-tests.yml = .github\workflows\idempotency-tests.yml
.github\workflows\integration-tests.yml = .github\workflows\integration-tests.yml
.github\workflows\load-tests.yml = .github\workflows\load-tests.yml
.github\workflows\project-auto-add.yml = .github\workflows\project-auto-add.yml
.github\workflows\sonarcloud.yml = .github\workflows\sonarcloud.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -49,15 +71,32 @@ Global
{01ABDF23-60CD-4CE3-8DC7-8654C4BA1EE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01ABDF23-60CD-4CE3-8DC7-8654C4BA1EE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01ABDF23-60CD-4CE3-8DC7-8654C4BA1EE8}.Release|Any CPU.Build.0 = Release|Any CPU
{51296EA6-7A4A-2A93-5060-1FBD4CB31CC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51296EA6-7A4A-2A93-5060-1FBD4CB31CC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51296EA6-7A4A-2A93-5060-1FBD4CB31CC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51296EA6-7A4A-2A93-5060-1FBD4CB31CC5}.Release|Any CPU.Build.0 = Release|Any CPU
{C6BFB632-723D-238A-EA11-44E78644612E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6BFB632-723D-238A-EA11-44E78644612E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C6BFB632-723D-238A-EA11-44E78644612E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{C6BFB632-723D-238A-EA11-44E78644612E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C6BFB632-723D-238A-EA11-44E78644612E}.Release|Any CPU.Build.0 = Release|Any CPU
{C6BFB632-723D-238A-EA11-44E78644612E}.Release|Any CPU.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {85C3AE3B-2D83-4457-B2F9-E56D64F4E443}
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{944FCE5E-0CFA-4018-B353-E14FA1395007} = {EAAC5CEA-33B8-495D-9CD0-B36794B8AFE7}
{1F5FC53F-061A-4CED-8B53-FC5C63DBEBFF} = {EAAC5CEA-33B8-495D-9CD0-B36794B8AFE7}
{A2B2F620-BC5D-47EA-8B98-5A942EC9CA9A} = {2DF34BB8-B19F-4623-9E3D-9F59A14C0660}
{019E5612-7663-40A2-A2EA-46E39D31F0A2} = {2DF34BB8-B19F-4623-9E3D-9F59A14C0660}
{E4C108A5-A13F-4C3F-B32A-86210A4EC52A} = {2DF34BB8-B19F-4623-9E3D-9F59A14C0660}
{01ABDF23-60CD-4CE3-8DC7-8654C4BA1EE8} = {EAAC5CEA-33B8-495D-9CD0-B36794B8AFE7}
{51296EA6-7A4A-2A93-5060-1FBD4CB31CC5} = {2DF34BB8-B19F-4623-9E3D-9F59A14C0660}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {EAAC5CEA-33B8-495D-9CD0-B36794B8AFE7}
{C6BFB632-723D-238A-EA11-44E78644612E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {85C3AE3B-2D83-4457-B2F9-E56D64F4E443}
EndGlobalSection
EndGlobal
6 changes: 6 additions & 0 deletions tests/Kiota.NPM.IntegrationTests/.runsettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<xUnit>
<ShowLiveOutput>true</ShowLiveOutput>
</xUnit>
</RunSettings>
13 changes: 13 additions & 0 deletions tests/Kiota.NPM.IntegrationTests/Assets/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "kiota-consumer",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"@microsoft/kiota": "file:./microsoft-kiota-1.0.0.tgz"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we wildcard this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or add a command line option in the test to force it to be 1.0.0

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @garethj-msft, could you share more context on this?

}
}
207 changes: 207 additions & 0 deletions tests/Kiota.NPM.IntegrationTests/InstallTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using System;
using System.Diagnostics;
using System.IO;
using Xunit;
using Xunit.Abstractions;


namespace Kiota.NPM.IntegrationTests;

public class InstallTests
{
private readonly ITestOutputHelper _outputHelper;

public InstallTests(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper));
}

[Fact]
public void Install_Creates_Working_Bin_Command()
{
// This test first compiles then packs the Kiota project to a local folder created for
// the individual test run, such that there is a tarball waiting in the folder.
// Then it copies the package.json file from the Assets folder to the test folder.
// Then it runs npm install in the test folder.
// Finally, it asserts two things:
// Firstly, if the kiota command is available in the node_modules/.bin folder.
// Secondly, if the npx command can be used to run the kiota command successfully
// with the '--version' command line option which must successfully return a string which conforms to a pattern expressing the version of kiota.

// Create a temporary directory for the test
var testDir = Path.Combine(Path.GetTempPath(), $"kiota-npm-test-{Guid.NewGuid()}");
Directory.CreateDirectory(testDir);

try
{
// Pack the project using npm
var projectDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../vscode/npm-package"));
_outputHelper.WriteLine($"Running pack in {projectDir}");
var npmPackProcess = RunProcess("npm", $"pack --pack-destination {testDir}", projectDir);
_outputHelper.WriteLine("Checking if npm pack process exited successfully.");
Assert.Equal(0, npmPackProcess.exitCode);

// Copy package.json from Assets folder to test directory
var assetsDir = Path.Combine(AppContext.BaseDirectory, "../../../Assets");
File.Copy(Path.Combine(assetsDir, "package.json"), Path.Combine(testDir, "package.json"));

// Run npm install in the test directory
var npmProcess = RunProcess("npm", "install", testDir);
_outputHelper.WriteLine("Checking if npm install process exited successfully.");
Assert.Equal(0, npmProcess.exitCode);

// Assert 1: Check if kiota exists in node_modules/.bin
var kiotaPath = Path.Combine(testDir, "node_modules", ".bin", "kiota");
if (OperatingSystem.IsWindows())
{
kiotaPath += ".cmd"; // On Windows, the bin command is a .cmd file
}
_outputHelper.WriteLine($"Checking if Kiota command exists at {kiotaPath}.");
Assert.True(File.Exists(kiotaPath), $"Kiota command not found at {kiotaPath}");

// Assert 2: Run kiota --version using npx and check output
var npxProcess = RunProcess("npx", "kiota --version", testDir);
_outputHelper.WriteLine("Checking if npx process to run 'kiota --version' exited successfully.");
Assert.Equal(0, npxProcess.exitCode);

// Check if output matches version pattern (e.g., 1.2.3 or 1.2.3-preview.4)
var versionPattern = @"^\d+\.\d+\.\d+.*$";
_outputHelper.WriteLine("Checking if the output of 'kiota --version' matches the expected version pattern.");
Assert.Matches(versionPattern, npxProcess.output);
}
finally
{
// Clean up
try
{
Directory.Delete(testDir, true);
}
catch
{
// Best effort cleanup
}
}
}

private (int exitCode, string output) RunProcess(string fileName, string arguments, string workingDirectory)
{
// Try to find npm or npx in standard locations if not found directly
if ((fileName == "npm" || fileName == "npx") && OperatingSystem.IsWindows())
{
// Check common locations for npm/npx on Windows
string commandExtension = ".cmd";
string[] possiblePaths = new[]
{
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs", $"{fileName}{commandExtension}"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "nodejs", $"{fileName}{commandExtension}"),
// Add from npm global prefix
Path.Combine(Environment.GetEnvironmentVariable("APPDATA") ?? "", "npm", $"{fileName}{commandExtension}"),
// Try npm global installation path
Path.Combine(Environment.GetEnvironmentVariable("APPDATA") ?? "", "npm", "node_modules", "npm", "bin", $"{fileName}{commandExtension}")
};

foreach (var path in possiblePaths)
{
if (File.Exists(path))
{
_outputHelper.WriteLine($"Found {fileName} at {path}");
fileName = path;
break;
}
}
}
else if ((fileName == "npm" || fileName == "npx") && (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()))
{
// Check common locations for npm/npx on Linux/macOS
string[] possiblePaths = new[]
{
$"/usr/bin/{fileName}",
$"/usr/local/bin/{fileName}",
$"/opt/homebrew/bin/{fileName}", // Common on macOS with Homebrew
$"{Environment.GetEnvironmentVariable("HOME")}/.npm/bin/{fileName}", // User's npm bin directory
$"{Environment.GetEnvironmentVariable("HOME")}/.nvm/versions/node/*/bin/{fileName}" // nvm installations
};

foreach (var path in possiblePaths)
{
// Handle wildcard paths (for nvm)
if (path.Contains("*"))
{
var directory = Path.GetDirectoryName(path);
if (directory != null && Directory.Exists(directory.Replace("*", "")))
{
// Get the highest version directory
var dirs = Directory.GetDirectories(directory.Replace("*", ""));
if (dirs.Length > 0)
{
var exactPath = Path.Combine(dirs[^1], Path.GetFileName(path));
if (File.Exists(exactPath))
{
_outputHelper.WriteLine($"Found {fileName} at {exactPath}");
fileName = exactPath;
break;
}
}
}
continue;
}

if (File.Exists(path))
{
_outputHelper.WriteLine($"Found {fileName} at {path}");
fileName = path;
break;
}
}
}

// Fall back to PATH environment if still using the short name
if (fileName == "npm" || fileName == "npx")
{
// Log that we're using PATH resolution
_outputHelper.WriteLine($"Using PATH environment to resolve {fileName}");
}

using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

var outputBuilder = new StringWriter();

process.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
outputBuilder.WriteLine(args.Data);
}
};

process.ErrorDataReceived += (sender, args) =>
{
if (args.Data != null)
{
outputBuilder.WriteLine($"Error: {args.Data}");
}
};

if (!process.Start())
{
throw new InvalidOperationException($"Failed to start process '{fileName}' in directory '{workingDirectory}'");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();

process.WaitForExit();
return (process.ExitCode, outputBuilder.ToString().Trim());
}
}
}
Loading
Loading