Skip to content
Merged
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
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,64 @@ jobs:
--api-key "$NUGET_API_KEY" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate

deploy-stable:
name: Deploy Stable Packages
runs-on: ubuntu-latest
needs:
- build-test-pack
if: github.event_name == 'push' && needs.build-test-pack.result == 'success'
environment: nuget-release
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'

- name: Restore maintenance tool
run: dotnet restore tools/analyticsLibrary.NuGetMaintenance/analyticsLibrary.NuGetMaintenance.csproj

- name: Build maintenance tool
run: dotnet build -c Release --no-restore tools/analyticsLibrary.NuGetMaintenance/analyticsLibrary.NuGetMaintenance.csproj

- name: Download artifacts
uses: actions/download-artifact@v7
with:
name: nuget-packages-${{ github.run_id }}
path: ./artifacts

- name: Publish stable packages
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
if [ -z "$NUGET_API_KEY" ]; then
echo "ERROR: NUGET_API_KEY is not set; cannot publish stable packages."
exit 1
fi

for pkg in ./artifacts/*.nupkg; do
if [[ "$pkg" == *"-beta.pr-"* ]]; then
echo "Skipping prerelease package: $(basename "$pkg")"
continue
fi
dotnet nuget push "$pkg" \
--api-key "$NUGET_API_KEY" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
done

- name: Deprecate beta packages
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
if [ -z "$NUGET_API_KEY" ]; then
echo "NUGET_API_KEY is not set; skipping beta deprecation."
exit 0
fi
dotnet run -c Release --no-build --project tools/analyticsLibrary.NuGetMaintenance -- deprecate-betas
23 changes: 19 additions & 4 deletions src/analyticsLibrary.ReleaseTooling/NuGetGalleryDeprecationApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@ public static HttpRequestMessage CreateDeprecationRequest(
bool hasCriticalBugs,
bool isOther,
string? customMessage,
string alternatePackageId,
string alternatePackageVersion,
string? alternatePackageId,
string? alternatePackageVersion,
string apiKey)
{
if (versions.Count == 0)
{
throw new ArgumentException("At least one version is required.", nameof(versions));
}

var hasAltId = !string.IsNullOrEmpty(alternatePackageId);
var hasAltVersion = !string.IsNullOrEmpty(alternatePackageVersion);
if (hasAltId != hasAltVersion)
{
throw new ArgumentException(
"alternatePackageId and alternatePackageVersion must both be provided or both be omitted.");
}

var pairs = new List<KeyValuePair<string, string>>();
for (var i = 0; i < versions.Count; i++)
{
Expand All @@ -46,8 +54,15 @@ public static HttpRequestMessage CreateDeprecationRequest(
pairs.Add(new KeyValuePair<string, string>("message", customMessage));
}

pairs.Add(new KeyValuePair<string, string>("alternatePackageId", alternatePackageId));
pairs.Add(new KeyValuePair<string, string>("alternatePackageVersion", alternatePackageVersion));
if (!string.IsNullOrEmpty(alternatePackageId))
{
pairs.Add(new KeyValuePair<string, string>("alternatePackageId", alternatePackageId));
}

if (!string.IsNullOrEmpty(alternatePackageVersion))
{
pairs.Add(new KeyValuePair<string, string>("alternatePackageVersion", alternatePackageVersion));
Comment on lines +57 to +64
}
pairs.Add(new KeyValuePair<string, string>("listedVerb", "Unchanged"));

var url = string.Format(DeprecationEndpointTemplate, Uri.EscapeDataString(packageId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,40 @@ public void CreateDeprecationRequest_uses_put_form_urlencoded()
Assert.NotNull(req.Content);
Assert.Equal("application/x-www-form-urlencoded", req.Content.Headers.ContentType!.MediaType);
}

[Fact]
public void CreateDeprecationRequest_allows_both_alternate_fields_omitted()
{
using var req = NuGetGalleryDeprecationApi.CreateDeprecationRequest(
"Some.Package",
new[] { "1.0.0" },
isLegacy: false,
hasCriticalBugs: false,
isOther: true,
"msg",
alternatePackageId: null,
alternatePackageVersion: null,
"fake-key");

Assert.NotNull(req);
}

[Theory]
[InlineData("Some.Package", null)]
[InlineData(null, "2.0.0")]
public void CreateDeprecationRequest_throws_when_only_one_alternate_field_is_provided(
string? altId, string? altVersion)
{
Assert.Throws<ArgumentException>(() =>
NuGetGalleryDeprecationApi.CreateDeprecationRequest(
"Some.Package",
new[] { "1.0.0" },
isLegacy: false,
hasCriticalBugs: false,
isOther: true,
"msg",
altId,
altVersion,
"fake-key"));
}
}
112 changes: 93 additions & 19 deletions tools/analyticsLibrary.NuGetMaintenance/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,57 @@
using analyticsLibrary.ReleaseTooling;
using NuGet.Versioning;

if (args.Length < 2 || !string.Equals(args[0], "apply-deprecations", StringComparison.Ordinal))
{
Console.Error.WriteLine("Usage: analyticsLibrary.NuGetMaintenance apply-deprecations <releaseVersion>");
return 1;
}
var command = args.Length > 0 ? args[0] : string.Empty;

var releaseRaw = args[1];
var release = VersionDeprecationPlanner.TryParseReleaseVersion(releaseRaw);
if (release is null)
if (string.Equals(command, "apply-deprecations", StringComparison.Ordinal))
{
Console.Error.WriteLine($"Invalid release version: {releaseRaw}");
return 1;
if (args.Length < 2)
{
Console.Error.WriteLine("Usage: analyticsLibrary.NuGetMaintenance apply-deprecations <releaseVersion>");
return 1;
}

var releaseRaw = args[1];
var release = VersionDeprecationPlanner.TryParseReleaseVersion(releaseRaw);
if (release is null)
{
Console.Error.WriteLine($"Invalid release version: {releaseRaw}");
return 1;
}

var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
Console.Error.WriteLine("NUGET_API_KEY is not set.");
return 1;
}

using var http = new HttpClient();
return await RunAsync(http, apiKey, release, CancellationToken.None).ConfigureAwait(false);
}
else if (string.Equals(command, "deprecate-betas", StringComparison.Ordinal))
{
var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
Console.Error.WriteLine("NUGET_API_KEY is not set.");
return 1;
}

var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
using var http = new HttpClient();
return await DeprecateBetasAsync(http, apiKey, CancellationToken.None).ConfigureAwait(false);
}
else
{
Console.Error.WriteLine("NUGET_API_KEY is not set.");
Console.Error.WriteLine("Usage:");
Console.Error.WriteLine(" analyticsLibrary.NuGetMaintenance apply-deprecations <releaseVersion>");
Console.Error.WriteLine(" analyticsLibrary.NuGetMaintenance deprecate-betas");
return 1;
}

using var http = new HttpClient();
return await RunAsync(http, apiKey, release, CancellationToken.None).ConfigureAwait(false);

static async Task<int> RunAsync(HttpClient http, string apiKey, NuGetVersion releaseVersion, CancellationToken cancellationToken)
{
// 40 matches the NuGet Gallery API's per-request version limit for deprecation batches
const int chunkSize = 40;
foreach (var packageId in ReleaseConstants.PackageIds)
{
Expand Down Expand Up @@ -98,6 +123,55 @@ static async Task<int> RunAsync(HttpClient http, string apiKey, NuGetVersion rel
return 0;
}

static async Task<int> DeprecateBetasAsync(HttpClient http, string apiKey, CancellationToken cancellationToken)
{
// 40 matches the NuGet Gallery API's per-request version limit for deprecation batches
const int chunkSize = 40;
foreach (var packageId in ReleaseConstants.PackageIds)
{
IReadOnlyList<string> published;
try
{
published = await NuGetFlatContainerHttp.GetPublishedVersionsAsync(http, packageId, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to list versions for {packageId}: {ex.Message}");
return 1;
}

if (published.Count == 0)
{
Console.WriteLine($"No published versions found for {packageId}; skipping.");
continue;
}

var betas = published.Where(VersionDeprecationPlanner.IsCiBetaPrerelease).ToList();
foreach (var chunk in Chunk(betas, chunkSize))
{
var code = await SendDeprecationAsync(
http,
apiKey,
packageId,
chunk,
isLegacy: false,
hasCriticalBugs: false,
isOther: true,
ReleaseConstants.PrereleaseTestingDeprecationMessage,
alternateVersion: null,
cancellationToken)
.ConfigureAwait(false);
if (code != 0)
{
return code;
}
}
}

return 0;
}

static List<List<string>> Chunk(IReadOnlyList<string> items, int size)
{
var chunks = new List<List<string>>();
Expand Down Expand Up @@ -125,23 +199,23 @@ static async Task<int> SendDeprecationAsync(
bool hasCriticalBugs,
bool isOther,
string message,
NuGetVersion releaseVersion,
NuGetVersion? alternateVersion,
CancellationToken cancellationToken)
{
if (versions.Count == 0)
{
return 0;
}

var normalizedRelease = releaseVersion.ToNormalizedString();
var normalizedRelease = alternateVersion?.ToNormalizedString();
using var request = NuGetGalleryDeprecationApi.CreateDeprecationRequest(
packageId,
versions,
isLegacy,
hasCriticalBugs,
isOther,
message,
packageId,
normalizedRelease is not null ? packageId : null,
normalizedRelease,
apiKey);

Expand Down
Loading