() : response.Data.Versions.ToArray();
- }
-
- var exception = response.ErrorException ?? new Exception("There was an issue getting data from NuGet.");
-
- throw new HttpRequestException(
- exception.Message,
- inner: null,
- statusCode: response.StatusCode);
- }
-
- ///
- public void Dispose()
- {
- if (this.isDisposed)
- {
- return;
- }
-
- this.client.Dispose();
-
- this.isDisposed = true;
- }
-}
diff --git a/README.md b/README.md
index 62b8401..aa12f07 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
## **🤷🏼♂️ What is it? 🤷🏼♂️**
-### This GitHub action checks whether or not a NuGet package with a particular name and version exists in the public NuGet gallery package repository [nuget.org](https://www.nuget.org).
+### This GitHub action checks whether or not a package with a particular name and version exists in a public gallery package repository like [nuget.org](https://www.nuget.org).
@@ -29,7 +29,7 @@
> - [Defining outputs for jobs](https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs)
> - [Setting a step action output parameter](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter)
-🪧 Example 🪧
+🪧 NuGet Example 🪧
```yaml
name: Package Monster Action Sample
@@ -44,8 +44,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- - name: Check If Nuget Package Exists
- id: nuget-exists
+ - name: Check If Package Exists
+ id: package-exists
uses: KinsonDigital/PackageMonster@v1.0.0-preview.1
with:
package-name: MyPackage 👈🏻 # Required input
@@ -58,12 +58,53 @@ jobs:
# Output name for the Package Monster GitHub action 👇🏼
# _____|_____
# | |
- $nugetExists = "${{ steps.nuget-exists.outputs.nuget-exists }}";
+ $packageExists = "${{ steps.package-exists.outputs.package-exists }}";
- if ($nugetExists -eq "true") {
- Write-Host "The NuGet package exists!!";
+ if ($packageExists -eq "true") {
+ Write-Host "The package exists!!";
} else {
- Write-Host "The NuGet package does not exist!!";
+ Write-Host "The package does not exist!!";
+ }
+```
+
+---
+
+🪧 NPM Example 🪧
+
+```yaml
+name: Package Monster Action Sample
+
+on:
+ workflow_dispatch:
+
+jobs:
+ Test_Action:em
+ name: Test Package Monster GitHub Action
+ runs-on: ubuntu-latest 👈🏼 # Required (Refer to the note above)
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Check If NPM Package Exists
+ id: package-exists
+ uses: KinsonDigital/PackageMonster@v1.0.0-preview.1
+ with:
+ package-name: MyPackage 👈🏻 # Required input
+ version: 1.2.3 👈🏻 # Required input
+ source: npm
+ fail-when-not-found: true 👈🏻 # Optional input
+
+ - name: Print Output Result #PowerShell Core
+ shell: pwsh 👈🏼 # Must be explicit with the shell to use PowerShell on Ubuntu
+ run: |
+ # Output name for the Package Monster GitHub action 👇🏼
+ # _____|_____
+ # | |
+ $packageExists = "${{ steps.package-exists.outputs.package-exists }}";
+
+ if ($packageExists -eq "true") {
+ Write-Host "The package exists!!";
+ } else {
+ Write-Host "The package does not exist!!";
}
```
@@ -74,11 +115,14 @@ jobs:
## **➡️ Action Inputs ⬅️**
-| Input Name | Description | Required | Default Value |
-|---|:---------------------------------------------------------------------------|:---:|:---:|
-| `package-name` | The name of the NuGet package. | yes | N/A |
-| `version` | The version of the package. | yes | N/A |
-| `fail-when-not-found` | Will fail the job if the NuGet package of a specific version is not found. | no | false |
+| Input Name | Description | Required | Default Value | Notes |
+|-----------------------|:---------------------------------------------------------------------|:--------:|:-------------:|:-------:|
+| `package-name` | The name of the package. | yes | N/A | |
+| `version` | The version of the package. | yes | N/A | |
+| `source` | The source repository to check. | no | nuget | Valid options are: `nuget`, `npm`, or a custom url. If 'PACKAGE-NAME' is in the url, it will be replaced with the value from the `package-name` input parameter. |
+| `json-path` | The json path to extract the versions. | no | N/A | Required if `source` is set to a custom url. Refer to https://jsonpath.com for syntax. |
+| `fail-when-found` | Will fail the job if the package of a specific version is found. | no | false | |
+| `fail-when-not-found` | Will fail the job if the package of a specific version is not found. | no | false | |
@@ -87,7 +131,7 @@ jobs:
## **⬅️ Action Output ➡️**
-The name of the output is `nuget-exists` and it returns a `boolean` of `true` or `false`.
+The name of the output is `package-exists` and it returns a `boolean` of `true` or `false`.
Refer to the **Example** above for how to use the output of the action.
---
diff --git a/Testing/PackageMonsterTests/ActionInputTests.cs b/Testing/PackageMonsterTests/ActionInputTests.cs
index 1c9d3a6..bcdff88 100644
--- a/Testing/PackageMonsterTests/ActionInputTests.cs
+++ b/Testing/PackageMonsterTests/ActionInputTests.cs
@@ -32,12 +32,17 @@ public void Ctor_WhenConstructed_PropsHaveCorrectDefaultValuesAndDecoratedWithAt
inputs.PackageName.Should().BeEmpty();
typeof(ActionInputs).GetProperty(nameof(ActionInputs.Version)).Should().BeDecoratedWith();
inputs.GetAttrFromProp(nameof(ActionInputs.Version))
- .AssertOptionAttrProps("version", true, "The version of the NuGet package to check. This is not case-sensitive.");
+ .AssertOptionAttrProps("version", true, "The version of the package to check. This is not case-sensitive.");
inputs.PackageName.Should().BeEmpty();
typeof(ActionInputs).GetProperty(nameof(ActionInputs.FailWhenNotFound)).Should().BeDecoratedWith();
inputs.GetAttrFromProp(nameof(ActionInputs.FailWhenNotFound))
- .AssertOptionAttrProps("fail-when-not-found", false, false, "If true, will fail the workflow if the NuGet package of the requested version does not exist.");
+ .AssertOptionAttrProps("fail-when-not-found", false, false, "If true, will fail the workflow if the package of the requested version does not exist.");
+
+ inputs.PackageName.Should().BeEmpty();
+ typeof(ActionInputs).GetProperty(nameof(ActionInputs.FailWhenFound)).Should().BeDecoratedWith();
+ inputs.GetAttrFromProp(nameof(ActionInputs.FailWhenFound))
+ .AssertOptionAttrProps("fail-when-found", false, false, "If true, will fail the workflow if the package of the requested version does exist.");
}
#endregion
}
diff --git a/Testing/PackageMonsterTests/Exceptions/NugetNotFoundExceptionTests.cs b/Testing/PackageMonsterTests/Exceptions/PackageNotFoundExceptionTests.cs
similarity index 69%
rename from Testing/PackageMonsterTests/Exceptions/NugetNotFoundExceptionTests.cs
rename to Testing/PackageMonsterTests/Exceptions/PackageNotFoundExceptionTests.cs
index d1f86c2..d3f99bd 100644
--- a/Testing/PackageMonsterTests/Exceptions/NugetNotFoundExceptionTests.cs
+++ b/Testing/PackageMonsterTests/Exceptions/PackageNotFoundExceptionTests.cs
@@ -1,4 +1,4 @@
-//
+//
// Copyright (c) KinsonDigital. All rights reserved.
//
@@ -8,19 +8,19 @@ namespace PackageMonsterTests.Exceptions;
using PackageMonster.Exceptions;
///
-/// Tests the class.
+/// Tests the class.
///
-public class NugetNotFoundExceptionTests
+public class PackageNotFoundExceptionTests
{
#region Constructor Tests
[Fact]
public void Ctor_WithNoParam_CorrectlySetsExceptionMessage()
{
// Act
- var exception = new NugetNotFoundException();
+ var exception = new PackageNotFoundException();
// Assert
- exception.Message.Should().Be("The NuGet package was not found.");
+ exception.Message.Should().Be("The package was not found.");
exception.HResult.Should().Be(60);
}
@@ -28,7 +28,7 @@ public void Ctor_WithNoParam_CorrectlySetsExceptionMessage()
public void Ctor_WhenInvokedWithSingleMessageParam_CorrectlySetsMessage()
{
// Act
- var exception = new NugetNotFoundException("test-message");
+ var exception = new PackageNotFoundException("test-message");
// Assert
exception.Message.Should().Be("test-message");
@@ -42,7 +42,7 @@ public void Ctor_WhenInvokedWithMessageAndInnerException_ThrowsException()
var innerException = new Exception("inner-exception");
// Act
- var deviceException = new NugetNotFoundException("test-exception", innerException);
+ var deviceException = new PackageNotFoundException("test-exception", innerException);
// Assert
deviceException.InnerException.Message.Should().Be("inner-exception");
diff --git a/Testing/PackageMonsterTests/GitHubActionTests.cs b/Testing/PackageMonsterTests/GitHubActionTests.cs
index 812ac0b..a0e2a28 100644
--- a/Testing/PackageMonsterTests/GitHubActionTests.cs
+++ b/Testing/PackageMonsterTests/GitHubActionTests.cs
@@ -14,7 +14,7 @@ namespace PackageMonsterTests;
public class GitHubActionTests
{
private readonly Mock mockConsoleService;
- private readonly Mock mockDataService;
+ private readonly Mock mockDataService;
private readonly Mock mockActionOutputService;
///
@@ -23,7 +23,7 @@ public class GitHubActionTests
public GitHubActionTests()
{
this.mockConsoleService = new Mock();
- this.mockDataService = new Mock();
+ this.mockDataService = new Mock();
this.mockActionOutputService = new Mock();
}
@@ -46,7 +46,7 @@ public async void Run_WhenInvoked_ShowsWelcomeMessage()
[InlineData("test-package", "4.5.6", "true")]
[InlineData("TEST-PACKAGE", "4.5.6", "true")]
[InlineData("test-package", "7.8.9", "false")]
- public async void Run_WhenNugetPackageWithVersionExists_SetsOutputToCorrectValue(
+ public async void Run_WhenPackageWithVersionExists_SetsOutputToCorrectValue(
string packageName,
string version,
string expectedOutput)
@@ -55,7 +55,7 @@ public async void Run_WhenNugetPackageWithVersionExists_SetsOutputToCorrectValue
var expectedSearchMsgStart = $"Searching for package '{packageName} v{version}' . . . ";
var expectedSearchMsgEnd = expectedOutput == "true" ? "package found!!" : "package not found!!";
- this.mockDataService.Setup(m => m.GetNugetVersions(packageName))
+ this.mockDataService.Setup(m => m.GetVersions(packageName, It.IsAny(), It.IsAny()))
.ReturnsAsync(new[] { "1.2.3", "4.5.6" });
var onCompletedInvoked = false;
@@ -67,14 +67,14 @@ public async void Run_WhenNugetPackageWithVersionExists_SetsOutputToCorrectValue
// Assert
await act.Should().NotThrowAsync();
- var expectedResultMsg = $"✅The NuGet package '{packageName}'";
+ var expectedResultMsg = $"✅The package '{packageName}'";
expectedResultMsg += $" with the version 'v{version}' was{(expectedOutput == "false" ? " not" : string.Empty)} found.";
this.mockConsoleService.VerifyOnce(m => m.Write(expectedSearchMsgStart, false));
this.mockConsoleService.VerifyOnce(m => m.WriteLine(expectedSearchMsgEnd));
this.mockConsoleService.VerifyOnce(m => m.WriteLine(expectedResultMsg));
this.mockConsoleService.Verify(m => m.BlankLine(), Times.Exactly(4));
- this.mockActionOutputService.VerifyOnce(m => m.SetOutputValue("nuget-exists", expectedOutput));
+ this.mockActionOutputService.VerifyOnce(m => m.SetOutputValue("package-exists", expectedOutput));
onCompletedInvoked.Should().BeTrue("the 'onCompleted()' was never invoked");
}
@@ -84,7 +84,7 @@ public async void Run_WhenPackageIsNotFoundWithFailSetToTrue_ThrowsExceptionWith
// Arrange
var inputs = CreateInputs(failWhenNotFound: true);
- this.mockDataService.Setup(m => m.GetNugetVersions(It.IsAny()))
+ this.mockDataService.Setup(m => m.GetVersions(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(Array.Empty());
var action = CreateAction();
@@ -94,8 +94,8 @@ public async void Run_WhenPackageIsNotFoundWithFailSetToTrue_ThrowsExceptionWith
// Assert
await act.Should()
- .ThrowAsync()
- .WithMessage($"The NuGet package '{inputs.PackageName}' with the version 'v{inputs.Version}' was not found.");
+ .ThrowAsync()
+ .WithMessage($"The package '{inputs.PackageName}' with the version 'v{inputs.Version}' was not found.");
}
[Theory]
@@ -106,7 +106,7 @@ public async void Run_WhenPackageIsNotFoundWithFailSetToFalseOrNull_DoesNotThrow
// Arrange
var inputs = CreateInputs(failWhenNotFound: failWhenNotFound);
- this.mockDataService.Setup(m => m.GetNugetVersions(It.IsAny()))
+ this.mockDataService.Setup(m => m.GetVersions(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(Array.Empty());
var action = CreateAction();
@@ -116,15 +116,15 @@ public async void Run_WhenPackageIsNotFoundWithFailSetToFalseOrNull_DoesNotThrow
// Assert
await act.Should()
- .NotThrowAsync();
+ .NotThrowAsync();
}
[Fact]
public async void Run_WhenAnExceptionIsThrown_InvokesOnErrorActionParam()
{
// Arrange
- this.mockDataService.Setup(m => m.GetNugetVersions(It.IsAny()))
- .Callback(_ => throw new Exception("test-exception"));
+ this.mockDataService.Setup(m => m.GetVersions(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((_, _, _) => throw new Exception("test-exception"));
Exception? exception = null;
var inputs = CreateInputs();
@@ -160,11 +160,13 @@ public void Dispose_WhenInvoked_DisposesOfAction()
private static ActionInputs CreateInputs(
string packageName = "test-package",
string version = "1.2.3",
- bool? failWhenNotFound = true) => new ()
+ bool? failWhenNotFound = true,
+ bool? failWhenFound = false) => new ()
{
PackageName = packageName,
Version = version,
FailWhenNotFound = failWhenNotFound,
+ FailWhenFound = failWhenFound
};
///
diff --git a/Testing/PackageMonsterTests/Models/NugetVersionsModelTests.cs b/Testing/PackageMonsterTests/Models/NugetVersionsModelTests.cs
deleted file mode 100644
index 7331220..0000000
--- a/Testing/PackageMonsterTests/Models/NugetVersionsModelTests.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-//
-// Copyright (c) KinsonDigital. All rights reserved.
-//
-
-// ReSharper disable UseObjectOrCollectionInitializer
-namespace PackageMonsterTests.Models;
-
-using FluentAssertions;
-using PackageMonster.Models;
-
-///
-/// Tests the class.
-///
-public class NugetVersionsModelTests
-{
- #region Prop Tests
- [Fact]
- public void Version_WhenSettingValue_ReturnsCorrectResult()
- {
- // Arrange
- var model = new NugetVersionsModel();
-
- // Act
- model.Versions = new[] { "1.2.3", "4.5.6" };
-
- // Assert
- model.Versions.Should()
- .HaveCount(2)
- .And.Contain("1.2.3")
- .And.Contain("4.5.6")
- .And.HaveElementPreceding("4.5.6", "1.2.3");
- }
- #endregion
-}
diff --git a/Testing/PackageMonsterTests/Models/PackageVersionsModelTests.cs b/Testing/PackageMonsterTests/Models/PackageVersionsModelTests.cs
new file mode 100644
index 0000000..e40947c
--- /dev/null
+++ b/Testing/PackageMonsterTests/Models/PackageVersionsModelTests.cs
@@ -0,0 +1,78 @@
+//
+// Copyright (c) KinsonDigital. All rights reserved.
+//
+
+// ReSharper disable UseObjectOrCollectionInitializer
+
+using Newtonsoft.Json.Linq;
+using PackageMonster.Repositories;
+using PackageMonster.Services;
+
+namespace PackageMonsterTests.Models;
+
+using FluentAssertions;
+
+///
+/// Tests the versions Json Path functionality.
+///
+public class PackageVersionsModelTests
+{
+ #region Prop Tests
+ [Fact]
+ public void Version_WhenUsingNugetJsonPath_ReturnsCorrectResult()
+ {
+ // Arrange
+ var packageRepository = new NugetPackageRepository();
+ var model = JObject.Parse(@"
+{
+ ""versions"": [
+ ""1.2.3"",
+ ""4.5.6""
+ ]
+}
+");
+
+ // Act
+ var actual = model.SelectTokens(packageRepository.JsonPath).Select(v => v.Value()).ToArray();
+
+ // Assert
+ actual.Should()
+ .HaveCount(2)
+ .And.Contain("1.2.3")
+ .And.Contain("4.5.6")
+ .And.HaveElementPreceding("4.5.6", "1.2.3");
+ }
+ #endregion
+
+
+ #region Prop Tests
+ [Fact]
+ public void Version_WhenUsingNpmJsonPath_ReturnsCorrectResult()
+ {
+ // Arrange
+ var packageRepository = new NpmPackageRepository();
+ var model = JObject.Parse(@"
+{
+ ""versions"": {
+ ""1.2.3"": {
+ ""version"": ""1.2.3""
+ },
+ ""4.5.6"": {
+ ""version"": ""4.5.6""
+ }
+ }
+}
+");
+
+ // Act
+ var actual = model.SelectTokens(packageRepository.JsonPath).Select(v => v.Value()).ToArray();
+
+ // Assert
+ actual.Should()
+ .HaveCount(2)
+ .And.Contain("1.2.3")
+ .And.Contain("4.5.6")
+ .And.HaveElementPreceding("4.5.6", "1.2.3");
+ }
+ #endregion
+}
diff --git a/Testing/PackageMonsterTests/Services/ActionOutputServiceTests.cs b/Testing/PackageMonsterTests/Services/ActionOutputServiceTests.cs
index 863d3b5..cecc01d 100644
--- a/Testing/PackageMonsterTests/Services/ActionOutputServiceTests.cs
+++ b/Testing/PackageMonsterTests/Services/ActionOutputServiceTests.cs
@@ -15,6 +15,7 @@ public class ActionOutputServiceTests
{
private readonly Mock mockEnvVarService;
private readonly Mock mockFile;
+ private readonly Mock mockConsoleService;
///
/// Initializes a new instance of the class.
@@ -23,6 +24,7 @@ public ActionOutputServiceTests()
{
this.mockEnvVarService = new Mock();
this.mockFile = new Mock();
+ this.mockConsoleService = new Mock();
}
#region Constructor Tests
@@ -32,7 +34,7 @@ public void Ctor_WithNullEnvVarServiceParam_ThrowsException()
// Arrange & Act
var act = () =>
{
- _ = new ActionOutputService(null, Mock.Of());
+ _ = new ActionOutputService(null, Mock.Of(), Mock.Of());
};
// Assert
@@ -47,7 +49,7 @@ public void Ctor_WithFileParam_ThrowsException()
// Arrange & Act
var act = () =>
{
- _ = new ActionOutputService(Mock.Of(), null);
+ _ = new ActionOutputService(Mock.Of(), null, null);
};
// Assert
@@ -55,6 +57,21 @@ public void Ctor_WithFileParam_ThrowsException()
.Throw()
.WithMessage("The parameter must not be null. (Parameter 'file')");
}
+
+ [Fact]
+ public void Ctor_WithConsoleServiceParam_ThrowsException()
+ {
+ // Arrange & Act
+ var act = () =>
+ {
+ _ = new ActionOutputService(Mock.Of(), Mock.Of(), null);
+ };
+
+ // Assert
+ act.Should()
+ .Throw()
+ .WithMessage("The parameter must not be null. (Parameter 'consoleService')");
+ }
#endregion
#region Method Tests
@@ -75,6 +92,23 @@ public void SetOutputValue_WithNullOrEmptyOutputName_ThrowsException(string name
.WithMessage("The parameter 'name' must not be null or empty.");
}
+ [Fact]
+ public void SetOutputValue_WhenOutputPathNotSpecified_LogWarning()
+ {
+ // Arrange
+ const string outputPath = "";
+ this.mockEnvVarService
+ .Setup(m => m.GetEnvironmentVariable(It.IsAny(), It.IsAny()))
+ .Returns(outputPath);
+ var sut = CreateSystemUnderTest();
+
+ // Act
+ sut.SetOutputValue("test-output", "test-value");
+
+ // Assert
+ this.mockConsoleService.VerifyOnce(m => m.WriteLine("WARNING: The environment variable 'GITHUB_OUTPUT' was not specified."));
+ }
+
[Fact]
public void SetOutputValue_WhenOutputPathDoesNotExist_ThrowsException()
{
@@ -91,18 +125,16 @@ public void SetOutputValue_WhenOutputPathDoesNotExist_ThrowsException()
// Assert
act.Should().Throw()
- .WithMessage("The GitHub output environment file was not found.");
+ .WithMessage("The GitHub output file was not found.");
}
[Fact]
public void SetOutputValue_WhenInvoked_SetsOutputValue()
{
// Arrange
- var expected =
- $"""
- other-output=other-value
- test-output=test-value{Environment.NewLine}
- """;
+ var expected = @"other-output=other-value
+test-output=test-value
+".ReplaceLineEndings(Environment.NewLine);
const string outputPath = "test-path";
var lines = new[]
@@ -134,5 +166,5 @@ public void SetOutputValue_WhenInvoked_SetsOutputValue()
///
/// The instance to test.
private ActionOutputService CreateSystemUnderTest() =>
- new (this.mockEnvVarService.Object, this.mockFile.Object);
+ new (this.mockEnvVarService.Object, this.mockFile.Object, this.mockConsoleService.Object);
}
diff --git a/action.yml b/action.yml
index 95a76a5..c1bd19d 100644
--- a/action.yml
+++ b/action.yml
@@ -1,5 +1,5 @@
name: 'PackageMonster'
-description: 'Checks if a NuGet package exists in the nuget.org public repository.'
+description: 'Checks if a package exists in the a public repository.'
author: 'Calvin Wilkinson (KinsonDigital)'
branding:
icon: settings
@@ -9,16 +9,27 @@ inputs:
description: 'The name of the package. This is not case-sensitive.'
required: true
version:
- description: 'The version of the NuGet package to check. This is not case-sensitive.'
+ description: 'The version of the package to check. This is not case-sensitive.'
required: true
+ source:
+ description: 'The source repository to check. The string `PACKAGE-NAME` will be replaced with the value from the `package-name` input parameter. Options: [ `nuget`, `npm`, custom url ]'
+ required: false
+ default: nuget
+ json-path:
+ description: 'The json path to extract the versions. Required only if a custom source url is specified.'
+ required: false
fail-when-not-found:
- description: 'If true, will fail the workflow if the NuGet package of the requested version does not exist.'
+ description: 'If true, will fail the workflow if the package of the requested version does not exist.'
+ required: false
+ default: 'false'
+ fail-when-found:
+ description: 'If true, will fail the workflow if the package of the requested version does exist.'
required: false
- default: false
+ default: 'false'
outputs:
- nuget-exists:
- description: 'True if the NuGet package exists.'
+ package-exists:
+ description: 'True if the package exists.'
# These are the arguments that are passed into the console app
runs:
@@ -29,5 +40,11 @@ runs:
- ${{ inputs.package-name }}
- '--version'
- ${{ inputs.version }}
+ - '--source'
+ - ${{ inputs.source }}
+ - '--json-path'
+ - ${{ inputs.json-path }}
- '--fail-when-not-found'
- ${{ inputs.fail-when-not-found }}
+ - '--fail-when-found'
+ - ${{ inputs.fail-when-found }}