Skip to content

Commit 84b9b6e

Browse files
committed
Configure package version update with delay
1 parent da0f277 commit 84b9b6e

File tree

9 files changed

+342
-32
lines changed

9 files changed

+342
-32
lines changed

.github/workflows/auto_bump_test_package_versions.yml.disabled

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,21 @@ jobs:
3838
global-json-file: global.json
3939

4040
- name: "Regenerating package versions"
41-
run: .\tracer\build.ps1 GeneratePackageVersions
41+
run: .\tracer\build.ps1 GeneratePackageVersions --PackageVersionCooldownDays 14
42+
43+
- name: Read cooldown report
44+
id: cooldown
45+
if: always()
46+
shell: pwsh
47+
run: |
48+
$report = ""
49+
$reportPath = "tracer/build/cooldown_report.md"
50+
if (Test-Path $reportPath) {
51+
$report = Get-Content $reportPath -Raw
52+
}
53+
"report<<EOF" >> $env:GITHUB_OUTPUT
54+
$report >> $env:GITHUB_OUTPUT
55+
"EOF" >> $env:GITHUB_OUTPUT
4256

4357
- name: Create Pull Request
4458
id: pr
@@ -55,6 +69,8 @@ jobs:
5569
body: |
5670
Updates the package versions for integration tests.
5771

72+
${{ steps.cooldown.outputs.report }}
73+
5874
- name: Send Slack notification about generating failure
5975
if: failure()
6076
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,6 @@ tools/dumps/
405405

406406
# test optimization temp/run folder
407407
.dd/
408+
409+
# Generated by GeneratePackageVersions target (consumed by CI workflow, not committed)
410+
tracer/build/cooldown_report.md

tracer/build/_build/Build.Utilities.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ partial class Build
5959
[Parameter("Only update package versions for packages with the following names")]
6060
readonly string[] IncludePackages;
6161

62+
[Parameter("Minimum age in days a NuGet package version must have been published before auto-including (default: 0, no filtering)")]
63+
readonly int PackageVersionCooldownDays;
64+
6265
[LazyLocalExecutable(@"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\gacutil.exe")]
6366
readonly Lazy<Tool> GacUtil;
6467
[LazyLocalExecutable(@"C:\Program Files\IIS Express\iisexpress.exe")]
@@ -247,11 +250,45 @@ partial class Build
247250
var previousSupportedVersions = await GenerateSupportMatrix.LoadPreviousVersions(supportedVersionsPath);
248251
Logger.Information("Loaded previous supported versions with {Count} entries", previousSupportedVersions.Count);
249252

253+
// Derive baseline from supported_versions.json: the max tested version per package
254+
// acts as a floor to prevent cooldown filtering from downgrading previously accepted versions.
255+
var baseline = previousSupportedVersions
256+
.Where(kvp => kvp.Value.MaxVersionTestedInclusive is not null)
257+
.GroupBy(kvp => kvp.Key.PackageName)
258+
.ToDictionary(
259+
g => g.Key,
260+
g => g.Max(kvp => new Version(kvp.Value.MaxVersionTestedInclusive!)));
261+
Logger.Information("Derived version baseline with {Count} entries from supported_versions.json", baseline.Count);
262+
250263
// Pipeline A: generate .g.props/.g.cs files
251-
var versionGenerator = new PackageVersionGenerator(TracerDirectory, testDir, shouldUpdatePackage, previousVersionCache);
264+
Logger.Information("Using package version cooldown of {Days} days", PackageVersionCooldownDays);
265+
var versionGenerator = new PackageVersionGenerator(TracerDirectory, testDir, shouldUpdatePackage, previousVersionCache, PackageVersionCooldownDays, baseline);
252266
var testedVersions = await versionGenerator.GenerateVersions(Solution);
253267
await NuGetVersionCache.Save(cacheFilePath, versionGenerator.VersionCache);
254268

269+
if (versionGenerator.CooldownReport.HasEntries)
270+
{
271+
Logger.Warning(
272+
"{Count} package version(s) were excluded due to the {Days}-day cooldown period",
273+
versionGenerator.CooldownReport.Entries.Count,
274+
PackageVersionCooldownDays);
275+
276+
foreach (var entry in versionGenerator.CooldownReport.Entries)
277+
{
278+
var resolvedText = entry.ResolvedVersion is not null ? $"using: {entry.ResolvedVersion}" : "skipped";
279+
Logger.Warning(
280+
" {Package} {Version} overridden (published {Date}, {Resolved})",
281+
entry.PackageName,
282+
entry.OverriddenVersion,
283+
entry.PublishedDate?.ToString("yyyy-MM-dd") ?? "unknown",
284+
resolvedText);
285+
}
286+
287+
var reportPath = BuildDirectory / "cooldown_report.md";
288+
await versionGenerator.CooldownReport.SaveToFile(reportPath);
289+
Logger.Information("Cooldown report saved to {Path}", reportPath);
290+
}
291+
255292
var assemblies = MonitoringHomeDirectory
256293
.GlobFiles("**/Datadog.Trace.dll")
257294
.Select(x => x.ToString())
@@ -260,6 +297,8 @@ partial class Build
260297
var integrations = GenerateIntegrationDefinitions.GetAllIntegrations(assemblies, definitionsFile);
261298

262299
// Pipeline B: generate dependabot files + supported_versions.json
300+
// TestedVersions are cooldown-filtered but the baseline prevents downgrades,
301+
// so they accurately reflect what we're testing.
263302
var distinctIntegrations = await DependabotFileManager.BuildDistinctIntegrationMaps(
264303
integrations, testedVersions, shouldUpdatePackage, previousSupportedVersions);
265304

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// <copyright file="CooldownReport.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.IO;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
namespace GeneratePackageVersions;
13+
14+
/// <summary>
15+
/// Collects package versions that were excluded by the cooldown filter
16+
/// and renders them as a markdown report for inclusion in the PR body.
17+
/// </summary>
18+
public class CooldownReport
19+
{
20+
private readonly int _cooldownDays;
21+
private readonly List<CooldownEntry> _entries = new();
22+
private readonly HashSet<(string PackageName, string Version)> _seen = new();
23+
24+
public CooldownReport(int cooldownDays)
25+
{
26+
_cooldownDays = cooldownDays;
27+
}
28+
29+
public bool HasEntries => _entries.Count > 0;
30+
31+
public IReadOnlyList<CooldownEntry> Entries => _entries;
32+
33+
public void Add(CooldownEntry entry)
34+
{
35+
// Deduplicate: the same version gets flagged once per framework and per selection group,
36+
// but we only need to report it once.
37+
if (_seen.Add((entry.PackageName, entry.OverriddenVersion)))
38+
{
39+
_entries.Add(entry);
40+
}
41+
}
42+
43+
public string ToMarkdown()
44+
{
45+
if (_entries.Count == 0)
46+
{
47+
return string.Empty;
48+
}
49+
50+
var sb = new StringBuilder();
51+
sb.AppendLine($"## Package Version Cooldown Report");
52+
sb.AppendLine();
53+
sb.AppendLine($"The following versions were published less than **{_cooldownDays} days** ago and have been overridden.");
54+
sb.AppendLine("These require manual review before inclusion.");
55+
sb.AppendLine();
56+
sb.AppendLine("| Package | Integration | Overridden Version | Published | Age (days) | Using Instead |");
57+
sb.AppendLine("|---------|-------------|--------------------|-----------|------------|---------------|");
58+
59+
foreach (var entry in _entries)
60+
{
61+
var published = entry.PublishedDate?.ToString("yyyy-MM-dd") ?? "unknown";
62+
var age = entry.PublishedDate.HasValue
63+
? ((int)(DateTimeOffset.UtcNow - entry.PublishedDate.Value).TotalDays).ToString()
64+
: "?";
65+
var usingInstead = entry.ResolvedVersion ?? "(skipped)";
66+
67+
sb.AppendLine($"| {entry.PackageName} | {entry.IntegrationName} | {entry.OverriddenVersion} | {published} | {age} | {usingInstead} |");
68+
}
69+
70+
return sb.ToString();
71+
}
72+
73+
public async Task SaveToFile(string path)
74+
{
75+
var markdown = ToMarkdown();
76+
if (!string.IsNullOrEmpty(markdown))
77+
{
78+
await File.WriteAllTextAsync(path, markdown);
79+
}
80+
}
81+
82+
public record CooldownEntry(
83+
string PackageName,
84+
string IntegrationName,
85+
string OverriddenVersion,
86+
DateTimeOffset? PublishedDate,
87+
string ResolvedVersion);
88+
}

tracer/build/_build/GeneratePackageVersions/NuGetPackageHelper.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,32 @@ namespace GeneratePackageVersions
1717
public class NuGetPackageHelper
1818
{
1919
/// <summary>
20-
/// Returns all available versions for a package, unfiltered by version range.
20+
/// Returns all available versions for a package with their publish dates, unfiltered by version range.
2121
/// Suitable for caching since the result is independent of any specific entry's version bounds.
2222
/// </summary>
23-
public static async Task<List<string>> GetAllNugetPackageVersions(string packageName)
23+
public static async Task<List<VersionWithDate>> GetAllNugetPackageVersions(string packageName)
2424
{
2525
var searchMetadata = await GetPackageMetadatas(packageName);
2626

27-
var packageVersions = new List<string>();
27+
var packageVersions = new List<VersionWithDate>();
2828
foreach (var md in searchMetadata)
2929
{
3030
if (md.Identity.HasVersion)
3131
{
32-
packageVersions.Add(md.Identity.Version.ToNormalizedString());
32+
packageVersions.Add(new VersionWithDate(
33+
md.Identity.Version.ToNormalizedString(),
34+
md.Published));
3335
}
3436
}
3537

3638
return packageVersions;
3739
}
3840

3941
/// <summary>
40-
/// Filters a list of version strings to only those within the entry's [MinVersion, MaxVersionExclusive) range.
42+
/// Filters a list of versions to only those within the entry's [MinVersion, MaxVersionExclusive) range.
43+
/// Preserves publish date metadata through the pipeline.
4144
/// </summary>
42-
public static List<string> FilterVersions(IEnumerable<string> allVersions, IPackageVersionEntry entry)
45+
public static List<VersionWithDate> FilterVersions(IEnumerable<VersionWithDate> allVersions, IPackageVersionEntry entry)
4346
{
4447
if (!NuGetVersion.TryParse(entry.MinVersion, out var minVersion))
4548
{
@@ -51,14 +54,14 @@ public static List<string> FilterVersions(IEnumerable<string> allVersions, IPack
5154
throw new ArgumentException($"MaxVersion {entry.MaxVersionExclusive} in integration {entry.IntegrationName} could not be parsed into a NuGet Version");
5255
}
5356

54-
var result = new List<string>();
55-
foreach (var versionText in allVersions)
57+
var result = new List<VersionWithDate>();
58+
foreach (var item in allVersions)
5659
{
57-
if (NuGetVersion.TryParse(versionText, out var version)
60+
if (NuGetVersion.TryParse(item.Version, out var version)
5861
&& version.CompareTo(minVersion) >= 0
5962
&& version.CompareTo(maxVersionExclusive) < 0)
6063
{
61-
result.Add(versionText);
64+
result.Add(item);
6265
}
6366
}
6467

tracer/build/_build/GeneratePackageVersions/NuGetVersionCache.cs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,57 @@ public static class NuGetVersionCache
2525

2626
/// <summary>
2727
/// Load the version cache from disk. Returns an empty dictionary if the file doesn't exist.
28+
/// Handles both the old format (plain version strings) and the new format (version + publish date).
2829
/// </summary>
29-
public static async Task<Dictionary<string, List<string>>> Load(string path)
30+
public static async Task<Dictionary<string, List<VersionWithDate>>> Load(string path)
3031
{
3132
if (!File.Exists(path))
3233
{
33-
return new Dictionary<string, List<string>>();
34+
return new Dictionary<string, List<VersionWithDate>>();
3435
}
3536

3637
await using var openStream = File.OpenRead(path);
3738

38-
var result = await JsonSerializer.DeserializeAsync<List<KeyValuePair<string, List<string>>>>(openStream, JsonOptions)
39+
// Try new format first: List<KeyValuePair<string, List<VersionWithDate>>>
40+
try
41+
{
42+
var result = await JsonSerializer.DeserializeAsync<List<KeyValuePair<string, List<VersionWithDate>>>>(openStream, JsonOptions);
43+
if (result is not null)
44+
{
45+
return new Dictionary<string, List<VersionWithDate>>(result);
46+
}
47+
}
48+
catch (JsonException)
49+
{
50+
// Fall through to old format
51+
}
52+
53+
// Reset stream position for retry with old format
54+
openStream.Position = 0;
55+
56+
// Old format: List<KeyValuePair<string, List<string>>>
57+
var legacy = await JsonSerializer.DeserializeAsync<List<KeyValuePair<string, List<string>>>>(openStream, JsonOptions)
3958
?? new List<KeyValuePair<string, List<string>>>();
40-
return new Dictionary<string, List<string>>(result);
59+
60+
return legacy.ToDictionary(
61+
kvp => kvp.Key,
62+
kvp => kvp.Value.Select(v => new VersionWithDate(v, null)).ToList());
4163
}
4264

4365
/// <summary>
4466
/// Save the version cache to disk.
4567
/// </summary>
46-
public static async Task Save(string path, Dictionary<string, List<string>> cache)
68+
public static async Task Save(string path, Dictionary<string, List<VersionWithDate>> cache)
4769
{
4870
// convert to a list to make sure it has deterministic ordering
4971
var ordered = cache
5072
.OrderBy(x => x.Key)
51-
.Select(x => new KeyValuePair<string, IEnumerable<string>>(
73+
.Select(x => new KeyValuePair<string, IEnumerable<VersionWithDate>>(
5274
x.Key,
5375
x.Value
54-
.Select(Version.Parse)
55-
.OrderBy(version => version)
56-
.Select(version => version.ToString())));
76+
.Select(v => v with { Version = Version.Parse(v.Version).ToString() })
77+
.OrderBy(v => Version.Parse(v.Version))
78+
.ToList()));
5779
await using var createStream = File.Create(path);
5880
await JsonSerializer.SerializeAsync(createStream, ordered, JsonOptions);
5981
}

0 commit comments

Comments
 (0)