From 8026f251756c97d0b3aee98d4c62bab480b8116e Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 16 May 2026 17:23:37 +0200 Subject: [PATCH 1/5] Add `Inedo.ProGet` sample + some small optimizations --- .gitignore | 3 + Directory.Build.props | 18 +++ Inedo.ProGet/BuildInfo.cs | 2 +- Inedo.ProGet/BuildIssue.cs | 2 +- Inedo.ProGet/FeedStorageType.cs | 2 +- Inedo.ProGet/Inedo.ProGet.csproj | 26 +--- Inedo.ProGet/PackageMetadata.cs | 2 +- Inedo.ProGet/PackageVersionInfo.cs | 20 +-- Inedo.ProGet/ProGetAuthenticationType.cs | 10 +- Inedo.ProGet/ProGetClient.cs | 2 +- Inedo.ProGet/ProGetHealthInfo.cs | 2 +- Inedo.ProGet/RetentionRule.cs | 2 +- Inedo.ProGet/ScaPermissionInfo.cs | 1 + Inedo.ProGet/Scan/BomWriter.cs | 2 +- .../Scan/ComposerDependencyScanner.cs | 7 +- Inedo.ProGet/Scan/DependencyScanner.cs | 18 +-- Inedo.ProGet/Scan/NpmDependencyScanner.cs | 8 +- Inedo.ProGet/SecurityTask.cs | 4 +- README.md | 4 + pgutil/ApiKeys/Create/SystemCommand.cs | 4 +- pgutil/ApiKeys/ListCommand.cs | 2 +- pgutil/Assets/Metadata/MetadataCommand.cs | 2 +- pgutil/Builds/BuildsCommand.cs | 2 +- pgutil/Builds/CreateCommand.cs | 20 +-- pgutil/Builds/ScanCommand.cs | 18 +-- pgutil/Config/PgUtilSource.cs | 2 +- pgutil/Feeds/CreateCommand.cs | 2 +- pgutil/Licenses/InfoCommand.cs | 2 +- pgutil/Packages/UploadCommand.cs | 2 +- pgutil/Security/Users/DeleteCommand.cs | 2 +- pgutil/Security/Users/EditCommand.cs | 2 +- pgutil/Sources/ListCommand.cs | 2 +- pgutil/Upack/UpackCommand.cs | 2 +- pgutil/pgutil.csproj | 49 +++---- samples/ProGetBootstrap.cs | 131 ++++++++++++++++++ samples/README.md | 15 ++ samples/compose.yml | 16 +++ 37 files changed, 288 insertions(+), 122 deletions(-) create mode 100644 Directory.Build.props create mode 100644 samples/ProGetBootstrap.cs create mode 100644 samples/README.md create mode 100644 samples/compose.yml diff --git a/.gitignore b/.gitignore index 1df55ce..470d7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -360,3 +360,6 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd /pgutil/Properties/launchSettings.json + +# Rider +.idea/* diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..131a516 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,18 @@ + + + net8.0;net10.0 + enable + enable + + Inedo + Inedo + ProGet + Copyright © Inedo 2026 + + https://github.com/Inedo/pgutil + https://github.com/Inedo/pgutil.git + git + MIT + proget + + \ No newline at end of file diff --git a/Inedo.ProGet/BuildInfo.cs b/Inedo.ProGet/BuildInfo.cs index 8943a9d..466f3ae 100644 --- a/Inedo.ProGet/BuildInfo.cs +++ b/Inedo.ProGet/BuildInfo.cs @@ -34,7 +34,7 @@ public sealed class BuildInfo { // Build version number (e.g. "1.0.0") public required string Version { get; init; } - + // Indicates whether the build is active // * Default is "true" // * Value is either "true" or "false" diff --git a/Inedo.ProGet/BuildIssue.cs b/Inedo.ProGet/BuildIssue.cs index 5fdbab1..bd7103b 100644 --- a/Inedo.ProGet/BuildIssue.cs +++ b/Inedo.ProGet/BuildIssue.cs @@ -43,7 +43,7 @@ public sealed class BuildIssue // A list of reasons for the issue (e.g. "Vulnerability (PGV-2422512); No license detected") public string? Detail { get; set; } - + // The package URL of the package associated with the issue (e.g. "pkg:npm/express@4.18.2") [JsonPropertyName("purl")] public required string PUrl { get; set; } diff --git a/Inedo.ProGet/FeedStorageType.cs b/Inedo.ProGet/FeedStorageType.cs index 1018cb7..70a05d7 100644 --- a/Inedo.ProGet/FeedStorageType.cs +++ b/Inedo.ProGet/FeedStorageType.cs @@ -37,7 +37,7 @@ public sealed class FeedStorageType // Name of the feed storage (e.g. "Local Disk", "Amazon S3") public string? Name { get; init; } - + // Description of the feed storage type (e.g. "Local file system path or network share.") public string? Description { get; init; } diff --git a/Inedo.ProGet/Inedo.ProGet.csproj b/Inedo.ProGet/Inedo.ProGet.csproj index 7720b1b..20bf0fc 100644 --- a/Inedo.ProGet/Inedo.ProGet.csproj +++ b/Inedo.ProGet/Inedo.ProGet.csproj @@ -1,31 +1,19 @@  - net8.0;net10.0 - enable - enable true true CS1591 - MIT - https://github.com/Inedo/pgutil - https://github.com/Inedo/pgutil.git - Inedo - Inedo - ProGet - Copyright © Inedo 2026 - proget - git - A HTTP Client that wraps ProGet's HTTP Endpoints. + A HTTP Client that wraps ProGet's HTTP Endpoints. -This is essentially the .NET-library version of pgutil, and is built from the pgutil GitHub code/repository. + This is essentially the .NET-library version of pgutil, and is built from the pgutil GitHub code/repository. -To learn how to use the library, see [Getting Started with Inedo.NuGet](https://docs.inedo.com/docs/proget-reference-api#net-library-nuget-package). + To learn how to use the library, see [Getting Started with Inedo.NuGet](https://docs.inedo.com/docs/proget-reference-api#net-library-nuget-package). - - - + + + - + \ No newline at end of file diff --git a/Inedo.ProGet/PackageMetadata.cs b/Inedo.ProGet/PackageMetadata.cs index b928ad0..977ef81 100644 --- a/Inedo.ProGet/PackageMetadata.cs +++ b/Inedo.ProGet/PackageMetadata.cs @@ -125,7 +125,7 @@ public sealed class PackageMetadataVulnerabilityAssessment // Severity of the assessment (E.g. "W", "E") public required string Severity { get; init; } - + // Indicates whether the vulnerability is blocked // * Values are either "true" or "false" public bool Blocked { get; init; } diff --git a/Inedo.ProGet/PackageVersionInfo.cs b/Inedo.ProGet/PackageVersionInfo.cs index 52e342a..22c5d2d 100644 --- a/Inedo.ProGet/PackageVersionInfo.cs +++ b/Inedo.ProGet/PackageVersionInfo.cs @@ -38,34 +38,34 @@ public sealed class PackageVersionInfo // PUrl of the package (see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst) [JsonPropertyName("purl")] public required string PUrl { get; init; } - + // Group of the package identifier if the package type supports groups public string? Group { get; init; } - + // Unique name of the package public required string Name { get; init; } - + // Version of the package public required string Version { get; init; } - + // Additional fields specific to the type of package (see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst) public string? Qualifier { get; init; } - + // Total number of downloads of all versions of the package from the ProGet feed public long TotalDownloads { get; init; } - + // Number of downloads of the latest version of the package from the ProGet feed public long Downloads { get; init; } - + // Timestamp when the package was published to ProGet public DateTime Published { get; init; } - + // User which published the package to ProGet; this information may not always be available public string? PublishedBy { get; init; } - + // Size of the package file in bytes public long Size { get; init; } - + // Indicates whether the package is visible to searches (assume true when not specified) public bool Listed { get; init; } = true; diff --git a/Inedo.ProGet/ProGetAuthenticationType.cs b/Inedo.ProGet/ProGetAuthenticationType.cs index 98a9ded..b5f70f9 100644 --- a/Inedo.ProGet/ProGetAuthenticationType.cs +++ b/Inedo.ProGet/ProGetAuthenticationType.cs @@ -1,8 +1,8 @@ namespace Inedo.ProGet; -public enum ProGetAuthenticationType -{ - None, - ApiKey, - UsernamePassword +public enum ProGetAuthenticationType +{ + None, + ApiKey, + UsernamePassword } \ No newline at end of file diff --git a/Inedo.ProGet/ProGetClient.cs b/Inedo.ProGet/ProGetClient.cs index fb16742..973cd03 100644 --- a/Inedo.ProGet/ProGetClient.cs +++ b/Inedo.ProGet/ProGetClient.cs @@ -55,7 +55,7 @@ public async Task GetInstanceHealthAsync(CancellationToken can { using var response = await this.http.GetAsync("health", cancellationToken).ConfigureAwait(false); using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - + try { return (await JsonSerializer.DeserializeAsync(stream, ProGetApiJsonContext.Default.ProGetHealthInfo, cancellationToken).ConfigureAwait(false))!; diff --git a/Inedo.ProGet/ProGetHealthInfo.cs b/Inedo.ProGet/ProGetHealthInfo.cs index b01939d..b10ca0f 100644 --- a/Inedo.ProGet/ProGetHealthInfo.cs +++ b/Inedo.ProGet/ProGetHealthInfo.cs @@ -60,7 +60,7 @@ public sealed class ProGetHealthInfo // Information on status of replication (if configured, else will be null). public ReplicationStatusInfo? ReplicationStatus { get; init; } - + // JSON Object used by ReplicationStatus property // * Status property values will be either "OK" or "Error" if replication servers exist, else will be null // * Error properties will return an error message if Status is "Error", else will be null diff --git a/Inedo.ProGet/RetentionRule.cs b/Inedo.ProGet/RetentionRule.cs index a4a826c..6307dbc 100644 --- a/Inedo.ProGet/RetentionRule.cs +++ b/Inedo.ProGet/RetentionRule.cs @@ -70,7 +70,7 @@ public sealed class RetentionRule // * When "false" and "sizeTriggerKb" is set to non-null "n", retention is run when the entire feed size is greater than `n` kilobytes // * This value is ignored when "sizeTriggerKb" is "null" public bool SizeExclusive { get; set; } - + // When set to "n", the retention rule always keeps versions that have been downloaded more than "n" times public int? TriggerDownloadCount { get; set; } diff --git a/Inedo.ProGet/ScaPermissionInfo.cs b/Inedo.ProGet/ScaPermissionInfo.cs index c8cf2d5..7edacf9 100644 --- a/Inedo.ProGet/ScaPermissionInfo.cs +++ b/Inedo.ProGet/ScaPermissionInfo.cs @@ -1,4 +1,5 @@ namespace Inedo.ProGet; + public sealed class ScaPermissionInfo { public bool CanView { get; init; } diff --git a/Inedo.ProGet/Scan/BomWriter.cs b/Inedo.ProGet/Scan/BomWriter.cs index abd34f9..e993367 100644 --- a/Inedo.ProGet/Scan/BomWriter.cs +++ b/Inedo.ProGet/Scan/BomWriter.cs @@ -74,7 +74,7 @@ public void AddPackage(string? group, string name, string version, string type, var fullName = string.IsNullOrEmpty(group) ? Uri.EscapeDataString(name) : $"{Uri.EscapeDataString(group)}/{Uri.EscapeDataString(name)}"; - this.writer.WriteElementString("purl", ns, $"pkg:{type}/{fullName}@{version}{(string.IsNullOrWhiteSpace(qualifier) ? string.Empty : ("?"+qualifier))}"); + this.writer.WriteElementString("purl", ns, $"pkg:{type}/{fullName}@{version}{(string.IsNullOrWhiteSpace(qualifier) ? string.Empty : ("?" + qualifier))}"); this.writer.WriteEndElement(); // component } diff --git a/Inedo.ProGet/Scan/ComposerDependencyScanner.cs b/Inedo.ProGet/Scan/ComposerDependencyScanner.cs index 3955967..0113417 100644 --- a/Inedo.ProGet/Scan/ComposerDependencyScanner.cs +++ b/Inedo.ProGet/Scan/ComposerDependencyScanner.cs @@ -2,6 +2,7 @@ using Inedo.DependencyScan; namespace Inedo.ProGet.Scan; + internal class ComposerDependencyScanner(CreateDependencyScannerArgs args) : DependencyScanner(args) { public override DependencyScannerType Type => DependencyScannerType.Composer; @@ -24,13 +25,13 @@ public override async Task> ResolveDependenc using var lockFileStream = await this.FileSystem.OpenReadAsync(composerLockFile.FullName, cancellationToken).ConfigureAwait(false); using var doc = await JsonDocument.ParseAsync(lockFileStream, cancellationToken: cancellationToken).ConfigureAwait(false); - if(!doc.RootElement.TryGetProperty("packages", out var packages) && packages.ValueKind != JsonValueKind.Array) + if (!doc.RootElement.TryGetProperty("packages", out var packages) && packages.ValueKind != JsonValueKind.Array) throw new DependencyScannerException($"composer.lock at {searchDirectory} does not contain any packages"); var dependencies = new List(); dependencies.AddRange(readDependencies(packages)); - - if(this.CreateArgs.IncludeDevDependencies && doc.RootElement.TryGetProperty("packages-dev", out var devPackages) && devPackages.ValueKind == JsonValueKind.Array) + + if (this.CreateArgs.IncludeDevDependencies && doc.RootElement.TryGetProperty("packages-dev", out var devPackages) && devPackages.ValueKind == JsonValueKind.Array) dependencies.AddRange(readDependencies(devPackages)); projects.Add(new ScannedProject(projectName.GetString()!, dependencies.Distinct())); diff --git a/Inedo.ProGet/Scan/DependencyScanner.cs b/Inedo.ProGet/Scan/DependencyScanner.cs index 9dfba36..92becf4 100644 --- a/Inedo.ProGet/Scan/DependencyScanner.cs +++ b/Inedo.ProGet/Scan/DependencyScanner.cs @@ -113,7 +113,7 @@ private static async Task GetImplicitFileAsync(DependencyScannerType sca { var files = await SourceFileSystem.Default.FindFilesAsync(folder, "requirements.txt", true, cancellationToken).ToListAsync(cancellationToken); if (files.Count == 1) - return files[0].FullName; + return files[0].FullName; if (files.Count > 1) throw new DependencyScannerException("Multiple requirements.txt files found in directory. Specify which requirements.txt file you would like to scan be using \"--input\" argument."); @@ -124,22 +124,22 @@ private static async Task GetImplicitFileAsync(DependencyScannerType sca private static async Task<(DependencyScannerType scannerType, string filePath)> GetImplicitTypeAsync(ISourceFileSystem fileSystem, string fileName, CancellationToken cancellationToken = default) { - if(fileSystem.IsDirectoryAsync(fileName)) + if (fileSystem.IsDirectoryAsync(fileName)) { var files = await fileSystem.FindFilesAsync(fileName, "*.slnx", true, cancellationToken).ToListAsync(cancellationToken); - if(files.Count == 1) + if (files.Count == 1) return (scannerType: DependencyScannerType.NuGet, filePath: files[0].FullName); else if (files.Count > 1) throw new DependencyScannerException("Multiple solution files found in directory. Specify which solution file you would like to scan be using \"--input\" argument."); files = await fileSystem.FindFilesAsync(fileName, "*.sln", true, cancellationToken).ToListAsync(cancellationToken); - if(files.Count == 1) + if (files.Count == 1) return (scannerType: DependencyScannerType.NuGet, filePath: files[0].FullName); else if (files.Count > 1) throw new DependencyScannerException("Multiple solution files found in directory. Specify which solution file you would like to scan be using \"--input\" argument."); files = await fileSystem.FindFilesAsync(fileName, "*.csproj", true, cancellationToken).ToListAsync(cancellationToken); - if(files.Count == 1) + if (files.Count == 1) return (scannerType: DependencyScannerType.NuGet, filePath: files[0].FullName); else if (files.Count > 1) throw new DependencyScannerException("Multiple project files found in directory. Specify which project file you would like to scan be using \"--input\" argument."); @@ -149,15 +149,15 @@ private static async Task GetImplicitFileAsync(DependencyScannerType sca return (scannerType: DependencyScannerType.Npm, filePath: files[0].FullName); files = await fileSystem.FindFilesAsync(fileName, "package-lock.json", true, cancellationToken).ToListAsync(cancellationToken); - if(files.Count > 0) + if (files.Count > 0) return (scannerType: DependencyScannerType.Npm, filePath: fileName); files = await fileSystem.FindFilesAsync(fileName, "Cargo.lock", true, cancellationToken).ToListAsync(cancellationToken); - if(files.Count > 0) + if (files.Count > 0) return (scannerType: DependencyScannerType.Cargo, filePath: fileName); - + files = await fileSystem.FindFilesAsync(fileName, "composer.lock", true, cancellationToken).ToListAsync(cancellationToken); - if(files.Count > 0) + if (files.Count > 0) return (scannerType: DependencyScannerType.Composer, filePath: fileName); files = await fileSystem.FindFilesAsync(fileName, "requirements.txt", true, cancellationToken).ToListAsync(cancellationToken); diff --git a/Inedo.ProGet/Scan/NpmDependencyScanner.cs b/Inedo.ProGet/Scan/NpmDependencyScanner.cs index c0558ce..59421c6 100644 --- a/Inedo.ProGet/Scan/NpmDependencyScanner.cs +++ b/Inedo.ProGet/Scan/NpmDependencyScanner.cs @@ -17,7 +17,7 @@ public override async Task> ResolveDependenc // Handle pnpm lock file if (this.SourcePath.EndsWith("pnpm-lock.yaml")) { - if(!await this.FileSystem.FileExistsAsync(this.SourcePath, cancellationToken)) + if (!await this.FileSystem.FileExistsAsync(this.SourcePath, cancellationToken)) throw new FileNotFoundException("The specified pnpm lock file was not found.", this.SourcePath); using var stream = await this.FileSystem.OpenReadAsync(this.SourcePath, cancellationToken).ConfigureAwait(false); using var reader = new StreamReader(stream); @@ -32,7 +32,7 @@ public override async Task> ResolveDependenc if (string.IsNullOrEmpty(projectName)) throw new InvalidOperationException($"Unable to determine project name from package.json in directory: {this.FileSystem.GetDirectoryName(this.SourcePath)}"); - + var dependencies = ReadPnpmLockFile(rootNode).ToList(); projects.Add(new ScannedProject(projectName, dependencies)); } @@ -46,7 +46,7 @@ public override async Task> ResolveDependenc ? this.FileSystem.GetDirectoryName(this.SourcePath) : this.SourcePath; - if(await this.FileSystem.FindFilesAsync(searchDirectory, "pnpm-lock.yaml", true, cancellationToken).AnyAsync(cancellationToken: cancellationToken)) + if (await this.FileSystem.FindFilesAsync(searchDirectory, "pnpm-lock.yaml", true, cancellationToken).AnyAsync(cancellationToken: cancellationToken)) Console.WriteLine("Warning: pnpm-lock.yaml file detected in the scan directory. To parse pNPM lock files, specify the the pnpm-lock.yaml file in the Source Path argument."); await foreach (var packageLockFile in this.FileSystem.FindFilesAsync(searchDirectory, "package-lock.json", !this.SourcePath.EndsWith("package-lock.json"), cancellationToken)) @@ -64,7 +64,7 @@ public override async Task> ResolveDependenc return projects; } - + } private IEnumerable ReadPackageLockFile(JsonDocument doc) diff --git a/Inedo.ProGet/SecurityTask.cs b/Inedo.ProGet/SecurityTask.cs index a2a03c0..c187f58 100644 --- a/Inedo.ProGet/SecurityTask.cs +++ b/Inedo.ProGet/SecurityTask.cs @@ -36,10 +36,10 @@ public sealed class SecurityTask public required string Name { get; init; } // Description of the task (e.g. "Allows basic feed access") - public string? Description { get; init; } + public string? Description { get; init; } // Indicates if the task is feed-scoped ("true") or global ("false") - public bool FeedScoped { get; init; } + public bool FeedScoped { get; init; } // The task attributes that the task enables (e.g. ["Feeds_ViewFeed", "Feeds_DownloadPackage"]) public required string[] Attributes { get; init; } diff --git a/README.md b/README.md index 5b50bbb..39dd73c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ It's built from the pgutil GitHub code/repository and is mostly a HTTP Client th To learn how to use the library, see [Getting Started with Inedo.NuGet](https://docs.inedo.com/docs/proget-reference-api#net-library-nuget-package). +# Samples + +You can find some [samples](samples) in the corresponding folder. + ## Pull Requests & Issues We're very open to your feedback and ideas for improving `pgutil` and `Inedo.ProGet`! diff --git a/pgutil/ApiKeys/Create/SystemCommand.cs b/pgutil/ApiKeys/Create/SystemCommand.cs index aadff05..ef96c3c 100644 --- a/pgutil/ApiKeys/Create/SystemCommand.cs +++ b/pgutil/ApiKeys/Create/SystemCommand.cs @@ -39,7 +39,7 @@ public static async Task ExecuteAsync(CommandContext context, CancellationT var apis = apiValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (apis.Length == 0 + if (apis.Length == 0 || apis.Contains("full-control") && apis.Length > 1 || apis.Any(a => a != "feeds" && a != "sca" && a != "sbom-upload" && a != "full-control")) { @@ -66,7 +66,7 @@ private sealed class ApisOption : IConsoleOption { public static bool Required => false; public static string Name => "--apis"; - public static string Description => + public static string Description => $"Specifies the individual APIs to give access to when creating a system API key. " + $"Value is either full-control or a comma-separated list of any combination of: feeds, sca, sbom-upload"; } diff --git a/pgutil/ApiKeys/ListCommand.cs b/pgutil/ApiKeys/ListCommand.cs index 9975a49..efec1a8 100644 --- a/pgutil/ApiKeys/ListCommand.cs +++ b/pgutil/ApiKeys/ListCommand.cs @@ -25,7 +25,7 @@ public static async Task ExecuteAsync(CommandContext context, CancellationT { count++; CM.WriteLine(key.DisplayName ?? "(unnamed key)"); - + var data = new List<(string, string)> { (" Id:", key.Id.ToString()!), diff --git a/pgutil/Assets/Metadata/MetadataCommand.cs b/pgutil/Assets/Metadata/MetadataCommand.cs index a696170..cc96a67 100644 --- a/pgutil/Assets/Metadata/MetadataCommand.cs +++ b/pgutil/Assets/Metadata/MetadataCommand.cs @@ -24,6 +24,6 @@ private sealed class PathOption : IConsoleOption public static string Name => "--path"; public static string Description => "Path of item to inspect"; } - } + } } } diff --git a/pgutil/Builds/BuildsCommand.cs b/pgutil/Builds/BuildsCommand.cs index d91bc07..78beb9a 100644 --- a/pgutil/Builds/BuildsCommand.cs +++ b/pgutil/Builds/BuildsCommand.cs @@ -92,7 +92,7 @@ private sealed class ScannerTypeOption : IConsoleOption public static string Description => "Type of project scanner to use; auto, npm, NuGet, PyPI, Conda, Composer, or Cargo (default=auto)"; public static string DefaultValue => "auto"; } - + private sealed class DoNotAuditFlag : IConsoleFlagOption { public static string Name => "--noaudit"; diff --git a/pgutil/Builds/CreateCommand.cs b/pgutil/Builds/CreateCommand.cs index 3e8ab23..8a871a5 100644 --- a/pgutil/Builds/CreateCommand.cs +++ b/pgutil/Builds/CreateCommand.cs @@ -35,16 +35,16 @@ public static async Task ExecuteAsync(CommandContext context, CancellationT else if (context.HasFlag()) active = false; - _ = await client.CreateOrUpdateBuildAsync( - new Inedo.ProGet.CreateOrUpdateBuildOptions - { - Project = context.GetOption(), - Version = context.GetOption(), - Active = active, - Stage = context.GetOptionOrDefault() - }, - cancellationToken - ); + _ = await client.CreateOrUpdateBuildAsync( + new Inedo.ProGet.CreateOrUpdateBuildOptions + { + Project = context.GetOption(), + Version = context.GetOption(), + Active = active, + Stage = context.GetOptionOrDefault() + }, + cancellationToken + ); Console.WriteLine("Build created."); return 0; diff --git a/pgutil/Builds/ScanCommand.cs b/pgutil/Builds/ScanCommand.cs index dac27d8..c8f9fca 100644 --- a/pgutil/Builds/ScanCommand.cs +++ b/pgutil/Builds/ScanCommand.cs @@ -36,14 +36,14 @@ public static void Configure(ICommandBuilder builder) public static async Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken) { - context.TryGetOption(out var input); + context.TryGetOption(out var input); CM.WriteLine("Scanning for dependencies in ", new TextSpan(input ?? Environment.CurrentDirectory, ConsoleColor.White), "..."); var scannerType = Enum.TryParse(context.GetOption(), out var _type) ? _type : DependencyScannerType.Auto; var scanner = await DependencyScanner.GetScannerAsync(new CreateDependencyScannerArgs( - input ?? string.Empty, - SourceFileSystem.Default, - IncludeProjectReferences: context.HasFlag(), + input ?? string.Empty, + SourceFileSystem.Default, + IncludeProjectReferences: context.HasFlag(), DoNotScanNodeModules: context.HasFlag(), IncludeDevDependencies: context.HasFlag() ), scannerType); @@ -66,7 +66,7 @@ public static async Task ExecuteAsync(CommandContext context, CancellationT await client.PublishSbomAsync(projects, consumer, context.GetOption(), scanner.Type.ToString().ToLowerInvariant(), cancellationToken); CM.WriteLine("SBOM published."); - if(context.HasFlag()) + if (context.HasFlag()) return 0; return await ExecuteAudit(client, consumer, cancellationToken); @@ -75,8 +75,8 @@ public static async Task ExecuteAsync(CommandContext context, CancellationT //Copy paste from AuditCommand.cs private static async Task ExecuteAudit(ProGetClient client, PackageConsumer consumer, CancellationToken cancellationToken) { - try - { + try + { var project = consumer.Name; var build = consumer.Version; @@ -149,7 +149,7 @@ private static async Task ExecuteAudit(ProGetClient client, PackageConsumer CM.WriteLine(" ", $"{v.Id} {v.Title}"); if (v.Category.HasValue) CM.Write($" Category {v.Category} ({v.Assessment})"); - if(v.Score.HasValue) + if (v.Score.HasValue) CM.Write($" Score {v.Score}"); } } @@ -176,7 +176,7 @@ private static async Task ExecuteAudit(ProGetClient client, PackageConsumer return -1; } } - catch (ProGetApiException ex) when(ex.StatusCode == HttpStatusCode.TooManyRequests) + catch (ProGetApiException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests) { CM.WriteError("ProGet Basic rate limit exceeded."); return 429; diff --git a/pgutil/Config/PgUtilSource.cs b/pgutil/Config/PgUtilSource.cs index bb279e3..4fe32b9 100644 --- a/pgutil/Config/PgUtilSource.cs +++ b/pgutil/Config/PgUtilSource.cs @@ -15,7 +15,7 @@ public ProGetClient GetProGetClient() return new(this.Url, this.Token); else if (!string.IsNullOrEmpty(this.Username)) return new(this.Url, this.Username, this.Password ?? string.Empty); - else + else return new ProGetClient(this.Url); } public PgUtilSource Obfuscate() diff --git a/pgutil/Feeds/CreateCommand.cs b/pgutil/Feeds/CreateCommand.cs index 40c2c9e..c0431a4 100644 --- a/pgutil/Feeds/CreateCommand.cs +++ b/pgutil/Feeds/CreateCommand.cs @@ -46,7 +46,7 @@ private sealed class TypeOption : IConsoleOption public static bool Required => true; public static string Name => "--type"; public static string Description => "Type of the feed to create"; - public static string[] ValidValues => ["NuGet", "Chocolatey", "npm", "Bower", "Maven", "Universal", "PowerShell", "Docker", "RubyGems", "VSIX", "Debian", "PyPI", "Helm", "RPM", "Conda", "APK", "CRAN", "Asset" ]; + public static string[] ValidValues => ["NuGet", "Chocolatey", "npm", "Bower", "Maven", "Universal", "PowerShell", "Docker", "RubyGems", "VSIX", "Debian", "PyPI", "Helm", "RPM", "Conda", "APK", "CRAN", "Asset"]; public static bool WarnWhenInvalidValue => true; } } diff --git a/pgutil/Licenses/InfoCommand.cs b/pgutil/Licenses/InfoCommand.cs index 9b3b1d0..e3396ec 100644 --- a/pgutil/Licenses/InfoCommand.cs +++ b/pgutil/Licenses/InfoCommand.cs @@ -38,7 +38,7 @@ public static async Task ExecuteAsync(CommandContext context, CancellationT Console.WriteLine("Detection:"); if (license.Spdx is not null) Console.WriteLine($" SPDX: {string.Join(", ", license.Spdx)}"); - + if (license.Urls is not null) { Console.WriteLine(" Url:"); diff --git a/pgutil/Packages/UploadCommand.cs b/pgutil/Packages/UploadCommand.cs index f845f7d..4ee2dc4 100644 --- a/pgutil/Packages/UploadCommand.cs +++ b/pgutil/Packages/UploadCommand.cs @@ -131,7 +131,7 @@ Stream getSource(out string? fileName) if (!inputFileName.Contains('*')) return [inputFileName]; - var fullPath = Path.GetFullPath(inputFileName); + var fullPath = Path.GetFullPath(inputFileName); int firstWildcardIndex = FirstWildcardRegex().Match(fullPath).Index; var rootPath = fullPath[..firstWildcardIndex]; var wildcardPart = fullPath[(firstWildcardIndex)..]; diff --git a/pgutil/Security/Users/DeleteCommand.cs b/pgutil/Security/Users/DeleteCommand.cs index 157a703..92c79de 100644 --- a/pgutil/Security/Users/DeleteCommand.cs +++ b/pgutil/Security/Users/DeleteCommand.cs @@ -16,7 +16,7 @@ private sealed class DeleteCommand : IConsoleCommand $> pgutil security users delete --username="John Smith" For more information, see: https://docs.inedo.com/docs/proget/api/security/users/delete - """; + """; public static void Configure(ICommandBuilder builder) { diff --git a/pgutil/Security/Users/EditCommand.cs b/pgutil/Security/Users/EditCommand.cs index 685f49b..805d77d 100644 --- a/pgutil/Security/Users/EditCommand.cs +++ b/pgutil/Security/Users/EditCommand.cs @@ -20,7 +20,7 @@ private sealed class EditCommand : IConsoleCommand For more information, see: https://docs.inedo.com/docs/proget/api/security/users/edit """; - + public static void Configure(ICommandBuilder builder) { builder.WithOption() diff --git a/pgutil/Sources/ListCommand.cs b/pgutil/Sources/ListCommand.cs index d2a7271..84ffa6e 100644 --- a/pgutil/Sources/ListCommand.cs +++ b/pgutil/Sources/ListCommand.cs @@ -38,7 +38,7 @@ 2. MyPackages (unapproved-nuget) { var s = sources[i]; - CM.Write(ConsoleColor.White, $"{i+1}. {s.Name}"); + CM.Write(ConsoleColor.White, $"{i + 1}. {s.Name}"); if (!string.IsNullOrEmpty(s.DefaultFeed)) CM.Write(new TextSpan(" ("), new TextSpan(s.DefaultFeed, ConsoleColor.White), new TextSpan(" feed)")); diff --git a/pgutil/Upack/UpackCommand.cs b/pgutil/Upack/UpackCommand.cs index f32ba5a..ba791e9 100644 --- a/pgutil/Upack/UpackCommand.cs +++ b/pgutil/Upack/UpackCommand.cs @@ -78,7 +78,7 @@ static void ExtractItems(Stream packgeStream, string targetPath, bool overwrite) try { - if(targetEntryPath.EndsWith("\\") || targetEntryPath.EndsWith("/")) + if (targetEntryPath.EndsWith("\\") || targetEntryPath.EndsWith("/")) Directory.CreateDirectory(targetEntryPath); else entry.ExtractToFile(targetEntryPath, overwrite); diff --git a/pgutil/pgutil.csproj b/pgutil/pgutil.csproj index 3eaf572..5ff9196 100644 --- a/pgutil/pgutil.csproj +++ b/pgutil/pgutil.csproj @@ -1,31 +1,20 @@  - - Exe - net8.0;net10.0 - enable - enable - PgUtil - Inedo - Inedo - ProGet - Copyright © Inedo 2026 - CLI for ProGet's APIs. - https://github.com/Inedo/pgutil - https://github.com/Inedo/pgutil.git - MIT - proget - README.md - true - true - 0.0.0 - - - - - - - - - - - + + Exe + PgUtil + CLI for ProGet's APIs. + README.md + true + true + 0.0.0 + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ProGetBootstrap.cs b/samples/ProGetBootstrap.cs new file mode 100644 index 0000000..a44e148 --- /dev/null +++ b/samples/ProGetBootstrap.cs @@ -0,0 +1,131 @@ +#!/usr/bin/dotnet run + +#:sdk Microsoft.NET.Sdk + +#:package Inedo.ProGet@2.1.3 + +#:property JsonSerializerIsReflectionEnabledByDefault=true + +using Inedo.ProGet; + +Console.WriteLine($"Starting ProGet bootstrap"); + +var client = new ProGetClient("http://localhost:8080", "Admin", "admin"); + +var groups = client.ListUserGroups(); +foreach (var group in DesiredState.Groups) +{ + if (await groups.FirstOrDefaultAsync(g => g.Name.Equals(group.Name)) != null) + { + await client.UpdateUserGroupAsync(group); + } + else + { + await client.CreateUserGroupAsync(group); + } +} + +var users = client.ListUsersAsync(); +foreach (var user in DesiredState.Users) +{ + if (await users.FirstOrDefaultAsync(u => u.Name.Equals(user.Name)) != null) + { + await client.UpdateUserAsync(user); + } + else + { + await client.CreateUserAsync(user); + } +} + +var connectors = client.ListConnectorsAsync(); +foreach (var connector in DesiredState.Connectors) +{ + if (await connectors.FirstOrDefaultAsync(c => c.Name.Equals(connector.Name)) != null) + { + await client.UpdateConnectorAsync(connector.Name, connector); + } + else + { + await client.CreateConnectorAsync(connector); + } +} + +var feeds = client.ListFeedsAsync(); +foreach (var feed in DesiredState.Feeds) +{ + if (await feeds.FirstOrDefaultAsync(f => f.Name!.Equals(feed.Name)) != null) + { + await client.UpdateFeedAsync(feed.Name!, feed); + } + else + { + await client.CreateFeedAsync(feed.Name!, feed.FeedType!); + await client.UpdateFeedAsync(feed.Name!, feed); + } +} + +var settings = client.ListSettingsAsync(); +foreach (var setting in DesiredState.Settings) +{ + await client.SetSettingAsync(setting.Name, setting.Value); +} + +Console.WriteLine("ProGet bootstrap completed successfully."); + +public static class DesiredState +{ + public static IReadOnlyList Users { get; } = + [ + new SecurityUser { + Name = "svc-ci", + DisplayName = "CI Service Account", + Email = "svc-ci@example.com", + Password = "svc-ci", + Groups = ["proget-admins"], + }, + ]; + + public static IReadOnlyList Groups { get; } = + [ + new SecurityGroup { + Name = "proget-admins", + }, + ]; + + public static IReadOnlyList Connectors { get; } = + [ + new ProGetConnector { + Name = "nuget-org", + FeedType = "nuget", + Url = "https://api.nuget.org/v3/index.json", + Timeout = 30, + MetadataCacheEnabled = true, + }, + ]; + + public static IReadOnlyList Feeds { get; } = + [ + new ProGetFeed { + Name = "nuget-internal", + FeedType = "NuGet", + Description = "Internal NuGet packages with nuget.org connector", + Active = true, + UseApiV3 = true, + Connectors = ["nuget-org"], + RetentionRulesEnabled = true, + VulnerabilitiesEnabled = true, + PackageStatisticsEnabled = true, + }, + ]; + + public static IReadOnlyList Settings { get; } = + [ + new SettingsInfo { + Name = "Web.BaseUrl", + Value = "http://localhost:8080", + Description = "Full root URL for this installation of ProGet. This should start with http:// or https://", + ValueType = SettingsInfoValueType.Text, + }, + ]; +} \ No newline at end of file diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..dcfe839 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,15 @@ +# Samples + +## Run ProGet vith your preferred cotnainer engine + +```bash +podman compose up +``` + +## Run the sample + +For the sample to work, you need to have ProGet running and setup a valid license key. + +```bash +dotnet run ProGetBootstrap.cs +``` \ No newline at end of file diff --git a/samples/compose.yml b/samples/compose.yml new file mode 100644 index 0000000..63bdae3 --- /dev/null +++ b/samples/compose.yml @@ -0,0 +1,16 @@ +services: + proget: + image: proget.inedo.com/productimages/inedo/proget:latest + container_name: proget + restart: unless-stopped + ports: + - "8080:80" + volumes: + - proget-packages:/var/proget/packages + - proget-database:/var/proget/database + - proget-backups:/var/proget/backups + +volumes: + proget-packages: + proget-database: + proget-backups: \ No newline at end of file From 1cdf89ce851e284a63b89383e01d84c4ea97164d Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 16 May 2026 17:31:34 +0200 Subject: [PATCH 2/5] Run `dotnet format` via pre-commit hook See https://github.com/bitwarden/server/blob/main/.git-hooks/pre-commit --- .git-hooks/pre-commit | 8 ++++++++ global.json | 6 ++++++ samples/README.md | 6 ++++-- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 .git-hooks/pre-commit create mode 100644 global.json diff --git a/.git-hooks/pre-commit b/.git-hooks/pre-commit new file mode 100644 index 0000000..344f031 --- /dev/null +++ b/.git-hooks/pre-commit @@ -0,0 +1,8 @@ +#!/bin/bash + +FILES=$(git diff --cached --name-only --diff-filter=ACM "*.cs") +if [ -n "$FILES" ] +then + dotnet format ./pgutil.slnx --no-restore --include $FILES + echo "$FILES" | xargs git add +fi \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..5949319 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.103", + "rollForward": "latestFeature" + } +} diff --git a/samples/README.md b/samples/README.md index dcfe839..8486110 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,6 +1,8 @@ # Samples -## Run ProGet vith your preferred cotnainer engine +## Run ProGet vith your preferred container engine + +E.g. ```bash podman compose up @@ -8,7 +10,7 @@ podman compose up ## Run the sample -For the sample to work, you need to have ProGet running and setup a valid license key. +For the sample to work, you need to have ProGet running and setup a valid license key. Once that's done: ```bash dotnet run ProGetBootstrap.cs From f02f9901d791c2dcc0c967d203d98dfae9e062ae Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 16 May 2026 17:38:24 +0200 Subject: [PATCH 3/5] Mark pre-commit hook as executable --- .git-hooks/pre-commit | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .git-hooks/pre-commit diff --git a/.git-hooks/pre-commit b/.git-hooks/pre-commit old mode 100644 new mode 100755 From 5e8ccddc4a82494fe4946f0ba38458feaac0029c Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 16 May 2026 17:45:47 +0200 Subject: [PATCH 4/5] Reword the README.md and samples --- README.md | 2 +- samples/README.md | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 39dd73c..87ca0b5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ To learn how to use the library, see [Getting Started with Inedo.NuGet](https:// # Samples -You can find some [samples](samples) in the corresponding folder. +Sample projects and examples are available in the [`samples`](samples) directory. ## Pull Requests & Issues diff --git a/samples/README.md b/samples/README.md index 8486110..302a1d6 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,16 +1,21 @@ # Samples -## Run ProGet vith your preferred container engine +## Start ProGet -E.g. +Before running the samples, start a local ProGet instance with your preferred container engine. + +For example: ```bash podman compose up ``` -## Run the sample +## Run a sample + +> [!IMPORTANT] +> The samples require a running ProGet instance with a valid license key configured. -For the sample to work, you need to have ProGet running and setup a valid license key. Once that's done: +After ProGet is running and initial setup is complete, run the sample with: ```bash dotnet run ProGetBootstrap.cs From 69efda161afd8fee913ace42fb5c715f4c72804b Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 16 May 2026 17:53:58 +0200 Subject: [PATCH 5/5] Use Central Package Management --- Directory.Packages.props | 13 +++++++++++++ Inedo.ProGet/Inedo.ProGet.csproj | 6 +++--- pgutil/pgutil.csproj | 4 ++-- 3 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 Directory.Packages.props diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..bdb9ad6 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,13 @@ + + + true + + + + + + + + + + \ No newline at end of file diff --git a/Inedo.ProGet/Inedo.ProGet.csproj b/Inedo.ProGet/Inedo.ProGet.csproj index 20bf0fc..5eeabc8 100644 --- a/Inedo.ProGet/Inedo.ProGet.csproj +++ b/Inedo.ProGet/Inedo.ProGet.csproj @@ -10,10 +10,10 @@ To learn how to use the library, see [Getting Started with Inedo.NuGet](https://docs.inedo.com/docs/proget-reference-api#net-library-nuget-package). - + - - + + \ No newline at end of file diff --git a/pgutil/pgutil.csproj b/pgutil/pgutil.csproj index 5ff9196..102aa3c 100644 --- a/pgutil/pgutil.csproj +++ b/pgutil/pgutil.csproj @@ -13,8 +13,8 @@ - - + + \ No newline at end of file