From 490f7edc40c9d5a587859c461ca4a28b34916299 Mon Sep 17 00:00:00 2001 From: Thomas Nieto <38873752+ThomasNieto@users.noreply.github.com> Date: Sat, 23 Mar 2024 20:19:40 -0500 Subject: [PATCH] Add Install, Update, Uninstall support --- .github/workflows/ci.yml | 126 +++++++++++++++++++++ .github/workflows/codeql.yml | 40 +++++++ BuildSettings.psd1 | 7 ++ src/code/AnyPackage.DotNet.ToolProvider.cs | 88 +++++++++++++- test/Find-Package.Tests.ps1 | 32 ++++++ test/Get-Package.Tests.ps1 | 27 +++++ test/Install-Package.Tests.ps1 | 21 ++++ test/Uninstall-Package.Tests.ps1 | 14 +++ test/Update-Package.Tests.ps1 | 25 ++++ 9 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 BuildSettings.psd1 create mode 100644 test/Find-Package.Tests.ps1 create mode 100644 test/Get-Package.Tests.ps1 create mode 100644 test/Install-Package.Tests.ps1 create mode 100644 test/Uninstall-Package.Tests.ps1 create mode 100644 test/Update-Package.Tests.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2d8b530 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +name: CI + +defaults: + run: + shell: pwsh + +on: + push: + branches: [ main ] + + pull_request: + branches: [ main ] + + release: + types: [ published ] + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: .NET Build + run: dotnet build --configuration Release + + - name: Create module + run: | + New-Item module -ItemType Directory + $settings = Import-PowerShellDataFile ./BuildSettings.psd1 + Copy-Item @settings + + - name: Upload module + uses: actions/upload-artifact@v3 + with: + name: module + path: ./module/ + + Test: + needs: Build + runs-on: windows-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Download module + uses: actions/download-artifact@v3 + with: + name: module + path: C:\Users\runneradmin\Documents\PowerShell\Modules\AnyPackage.DotNet.Tool\ + + - name: Install AnyPackage module + run: Install-Module AnyPackage -Force -AllowClobber + + - name: Test with Pester + run: | + $ht = Import-PowerShellDataFile PesterSettings.psd1 + $config = New-PesterConfiguration $ht + Invoke-Pester -Configuration $config + + Sign: + needs: Test + if: github.event_name == 'release' && github.event.action == 'published' + runs-on: windows-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Download module + uses: actions/download-artifact@v3 + with: + name: module + path: module + + - name: Import certificate + env: + CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }} + CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} + CERTIFICATE_PASSWORD_KEY_BASE64: ${{ secrets.CERTIFICATE_PASSWORD_KEY_BASE64 }} + run: | + [convert]::FromBase64String($env:CERTIFICATE_BASE64) | Set-Content -Path cert.pfx -AsByteStream + $key = [convert]::FromBase64String($env:CERTIFICATE_PASSWORD_KEY_BASE64) + $password = ConvertTo-SecureString $env:CERTIFICATE_PASSWORD -Key $key + Import-PfxCertificate cert.pfx -Password $password -CertStoreLocation Cert:\CurrentUser\My + + - name: Sign files + run: | + $config = Import-PowerShellDataFile SignSettings.psd1 + $config['Certificate'] = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert + Set-Location .\module + Set-AuthenticodeSignature @config + + - name: Create and sign catalog file + run: | + $config = Import-PowerShellDataFile SignSettings.psd1 + $config['FilePath'] = 'AnyPackage.DotNet.Tool.cat' + $config['Certificate'] = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert + Set-Location .\module + New-FileCatalog $config['FilePath'] -CatalogVersion 2 + Set-AuthenticodeSignature @config + + - name: Upload module + uses: actions/upload-artifact@v3 + with: + name: module-signed + path: ./module/ + + Publish: + needs: Sign + if: github.event_name == 'release' && github.event.action == 'published' + runs-on: ubuntu-latest + steps: + + - name: Download module + uses: actions/download-artifact@v3 + with: + name: module-signed + path: '~/.local/share/powershell/Modules/AnyPackage.DotNet.Tool' + + - name: Install AnyPackage module + run: Install-Module AnyPackage -Force -AllowClobber + + - name: Publish Module + env: + NUGET_KEY: ${{ secrets.NUGET_KEY }} + run: Publish-Module -Name AnyPackage.DotNet.Tool -NuGetApiKey $env:NUGET_KEY diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..67e1f3c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '32 19 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/BuildSettings.psd1 b/BuildSettings.psd1 new file mode 100644 index 0000000..2e851c9 --- /dev/null +++ b/BuildSettings.psd1 @@ -0,0 +1,7 @@ +@{ + Path = @( + './src/code/bin/Release/netstandard2.0/AnyPackage.DotNet.ToolProvider.dll', + './src/AnyPackage.DotNet.Tool.psd1' + ) + Destination = './module' +} diff --git a/src/code/AnyPackage.DotNet.ToolProvider.cs b/src/code/AnyPackage.DotNet.ToolProvider.cs index bbe2653..585420c 100644 --- a/src/code/AnyPackage.DotNet.ToolProvider.cs +++ b/src/code/AnyPackage.DotNet.ToolProvider.cs @@ -9,7 +9,7 @@ namespace AnyPackage.Provider.DotNet { [PackageProvider(".NET Tool")] - public class ToolProvider : PackageProvider, IFindPackage, IGetPackage + public class ToolProvider : PackageProvider, IFindPackage, IGetPackage, IInstallPackage, IUpdatePackage, IUninstallPackage { public void FindPackage(PackageRequest request) { @@ -50,7 +50,7 @@ public void FindPackage(PackageRequest request) if (!first) { dictionary["Versions"] = versions; - WritePackage(request, dictionary, versions); + WritePackageVersions(request, dictionary, versions); } first = false; @@ -78,7 +78,7 @@ public void FindPackage(PackageRequest request) if (!first) { dictionary["Versions"] = versions; - WritePackage(request, dictionary, versions); + WritePackageVersions(request, dictionary, versions); } } @@ -96,7 +96,7 @@ public void GetPackage(PackageRequest request) while ((line = reader.ReadLine()) is not null) { - var match = Regex.Match(line, @"^(?\S+)\s+(?\d\\S+)\s+(?.+)$"); + var match = Regex.Match(line, @"^(?\S+)\s+(?\d\S+)\s+(?.+)$"); if (match.Success && request.IsMatch(match.Groups["Name"].Value, match.Groups["Version"].Value)) { @@ -113,7 +113,85 @@ public void GetPackage(PackageRequest request) } } - private void WritePackage(PackageRequest request, Dictionary dictionary, Dictionary versions) + public void InstallPackage(PackageRequest request) + { + var args = $"tool install {request.Name} --global"; + + if (request.Version is not null) + { + args += $" --version {request.Version}"; + } + + if (request.Prerelease) + { + args += " --prerelease"; + } + + InvokeDotNet(request, args); + } + + public void UninstallPackage(PackageRequest request) + { + var args = $"tool uninstall {request.Name} --global"; + InvokeDotNet(request, args); + } + + public void UpdatePackage(PackageRequest request) + { + var args = $"tool update {request.Name} --global"; + + if (request.Version is not null) + { + args += $" --version {request.Version}"; + } + + if (request.Prerelease) + { + args += " --prerelease"; + } + + InvokeDotNet(request, args); + } + + private void InvokeDotNet(PackageRequest request, string args) + { + request.WriteVerbose($"Calling 'dotnet {args}'"); + + using var process = new Process(); + process.StartInfo.Arguments = args; + process.StartInfo.FileName = "dotnet"; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + process.WaitForExit(); + + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + + if (output.Length > 0) + { + request.WriteVerbose(output); + } + + if (Regex.IsMatch(error, "could not be found")) + { + return; + } + else if (error.Length > 0) + { + throw new PackageProviderException(error); + } + + var match = Regex.Match(output, @"Tool '(?\S+)'.+version '(?\S+)'"); + var package = new PackageInfo(match.Groups["Name"].Value, + match.Groups["Version"].Value, + ProviderInfo); + + request.WritePackage(package); + } + + private void WritePackageVersions(PackageRequest request, Dictionary dictionary, Dictionary versions) { foreach (var version in versions.Keys) { diff --git a/test/Find-Package.Tests.ps1 b/test/Find-Package.Tests.ps1 new file mode 100644 index 0000000..3ee7eb4 --- /dev/null +++ b/test/Find-Package.Tests.ps1 @@ -0,0 +1,32 @@ +#Requires -Modules AnyPackage.DotNet.Tool + +Describe Find-Package { + Context 'with -Prerelease parameter' { + It 'should return prerelease versions' { + Find-Package -Name PowerShell -Prerelease -WarningAction SilentlyContinue | + Select-Object -ExpandProperty Version | + Where-Object IsPrerelease -eq $true | + Should -Not -BeNullOrEmpty + } + } + + Context 'with -Name parameter' { + It 'single name' { + Find-Package -Name powershell | + Should -Not -BeNullOrEmpty + } + + It 'multiple names' { + Find-Package -Name powershell, microsoft.dotnet-interactive | + Select-Object -Property Name -Unique | + Should -HaveCount 2 + } + } + + Context 'with -Version parameter' { + It 'should return value' { + Find-Package -Name powershell -Version 7.4.1 | + Should -Not -BeNullOrEmpty + } + } +} diff --git a/test/Get-Package.Tests.ps1 b/test/Get-Package.Tests.ps1 new file mode 100644 index 0000000..cb9c203 --- /dev/null +++ b/test/Get-Package.Tests.ps1 @@ -0,0 +1,27 @@ +#Requires -Modules AnyPackage.DotNet.Tool + +Describe Get-Package { + BeforeAll { + dotnet tool install powerprepare.app --global + dotnet tool install ib --global + } + + AfterAll { + dotnet tool uninstall powerprepare.app --global + dotnet tool uninstall ib --global + } + + Context 'with no parameters' { + It 'should return results' { + Get-Package | + Should -Not -BeNullOrEmpty + } + } + + Context 'with -Name parameter' { + It 'should return powerprepare.app' { + Get-Package -Name powerprepare.app | + Should -Not -BeNullOrEmpty + } + } +} diff --git a/test/Install-Package.Tests.ps1 b/test/Install-Package.Tests.ps1 new file mode 100644 index 0000000..570ede1 --- /dev/null +++ b/test/Install-Package.Tests.ps1 @@ -0,0 +1,21 @@ +#Requires -Modules AnyPackage.DotNet.Tool + +Describe Install-Package { + AfterEach { + dotnet tool uninstall powerprepare.app --global + } + + Context 'with -Name parameter' { + It 'should install' { + { Install-Package -Name powerprepare.app } | + Should -Not -Throw + } + } + + Context 'with -Version parameter' { + It 'should install' { + { Install-Package -Name powerprepare.app -Version '1.0.1' -ErrorAction Stop } | + Should -Not -Throw + } + } +} diff --git a/test/Uninstall-Package.Tests.ps1 b/test/Uninstall-Package.Tests.ps1 new file mode 100644 index 0000000..f2724b7 --- /dev/null +++ b/test/Uninstall-Package.Tests.ps1 @@ -0,0 +1,14 @@ +#Requires -Modules AnyPackage.DotNet.Tool + +Describe Uninstall-Package { + BeforeEach { + dotnet tool install powerprepare.app --global + } + + Context 'with -Name parameter' { + It 'should uninstall' { + { Uninstall-Package -Name powerprepare.app } | + Should -Not -Throw + } + } +} diff --git a/test/Update-Package.Tests.ps1 b/test/Update-Package.Tests.ps1 new file mode 100644 index 0000000..14ae647 --- /dev/null +++ b/test/Update-Package.Tests.ps1 @@ -0,0 +1,25 @@ +#Requires -Modules AnyPackage.DotNet.Tool + +Describe Update-Package { + BeforeEach { + dotnet tool install powerprepare.app --global --version 1.0.1 + } + + AfterEach { + dotnet tool uninstall powerprepare.app --global + } + + Context 'without parameters' { + It 'should update' -Skip { + { Update-Package -ErrorAction Stop } | + Should -Not -Throw + } + } + + Context 'with -Name parameter' { + It 'should update' { + { Update-Package -Name powerprepare.app -ErrorAction Stop } | + Should -Not -Throw + } + } +}