Skip to content

Commit a4675d8

Browse files
authored
Add CI information to User-Agent header (#7103)
1 parent 8a9eabb commit a4675d8

3 files changed

Lines changed: 458 additions & 21 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using NuGet.Common;
6+
7+
namespace NuGet.Protocol.Core.Types
8+
{
9+
/// <summary>
10+
/// Detects CI/CD environment from environment variables.
11+
/// </summary>
12+
internal static class CIEnvironmentDetector
13+
{
14+
/// <summary>
15+
/// Detects the CI environment based on environment variables.
16+
/// </summary>
17+
/// <param name="environmentVariableReader">The environment variable reader to use.</param>
18+
/// <returns>A <see cref="string"/> if a CI environment is detected, null otherwise.</returns>
19+
internal static string? Detect(IEnvironmentVariableReader environmentVariableReader)
20+
{
21+
// GitHub Actions
22+
if (string.Equals(environmentVariableReader.GetEnvironmentVariable("GITHUB_ACTIONS"), "true", StringComparison.OrdinalIgnoreCase))
23+
{
24+
return "GitHub Actions";
25+
}
26+
27+
// Azure DevOps
28+
if (string.Equals(environmentVariableReader.GetEnvironmentVariable("TF_BUILD"), "true", StringComparison.OrdinalIgnoreCase))
29+
{
30+
return "Azure DevOps";
31+
}
32+
33+
// AppVeyor
34+
if (string.Equals(environmentVariableReader.GetEnvironmentVariable("APPVEYOR"), "true", StringComparison.OrdinalIgnoreCase))
35+
{
36+
return "AppVeyor";
37+
}
38+
39+
// Travis CI
40+
if (string.Equals(environmentVariableReader.GetEnvironmentVariable("TRAVIS"), "true", StringComparison.OrdinalIgnoreCase))
41+
{
42+
return "Travis CI";
43+
}
44+
45+
// CircleCI
46+
if (string.Equals(environmentVariableReader.GetEnvironmentVariable("CIRCLECI"), "true", StringComparison.OrdinalIgnoreCase))
47+
{
48+
return "CircleCI";
49+
}
50+
51+
// AWS CodeBuild
52+
if (!string.IsNullOrEmpty(environmentVariableReader.GetEnvironmentVariable("CODEBUILD_BUILD_ID")))
53+
{
54+
return "AWS CodeBuild";
55+
}
56+
57+
// Jenkins - requires both BUILD_ID and BUILD_URL
58+
if (!string.IsNullOrEmpty(environmentVariableReader.GetEnvironmentVariable("BUILD_ID")) &&
59+
!string.IsNullOrEmpty(environmentVariableReader.GetEnvironmentVariable("BUILD_URL")))
60+
{
61+
return "Jenkins";
62+
}
63+
64+
// Google Cloud Build - requires both BUILD_ID and PROJECT_ID
65+
if (!string.IsNullOrEmpty(environmentVariableReader.GetEnvironmentVariable("BUILD_ID")) &&
66+
!string.IsNullOrEmpty(environmentVariableReader.GetEnvironmentVariable("PROJECT_ID")))
67+
{
68+
return "Google Cloud";
69+
}
70+
71+
// TeamCity
72+
if (!string.IsNullOrEmpty(environmentVariableReader.GetEnvironmentVariable("TEAMCITY_VERSION")))
73+
{
74+
return "TeamCity";
75+
}
76+
77+
// JetBrains Space
78+
if (!string.IsNullOrEmpty(environmentVariableReader.GetEnvironmentVariable("JB_SPACE_API_URL")))
79+
{
80+
return "JetBrains Space";
81+
}
82+
83+
// GitLab CI
84+
85+
if (string.Equals(environmentVariableReader.GetEnvironmentVariable("GITLAB_CI"), "true", StringComparison.OrdinalIgnoreCase))
86+
{
87+
return "GitLab CI";
88+
}
89+
90+
// Generic CI - must be last as it's the most general
91+
if (string.Equals(environmentVariableReader.GetEnvironmentVariable("CI"), "true", StringComparison.OrdinalIgnoreCase))
92+
{
93+
return "other";
94+
}
95+
96+
return null;
97+
}
98+
}
99+
}

src/NuGet.Core/NuGet.Protocol/UserAgentStringBuilder.cs

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
#endif
99
using System.Globalization;
1010
using System.Runtime.InteropServices;
11+
using System.Text;
12+
using NuGet.Common;
1113
using NuGet.Packaging;
1214

1315
namespace NuGet.Protocol.Core.Types
@@ -16,27 +18,38 @@ public class UserAgentStringBuilder
1618
{
1719
public static readonly string DefaultNuGetClientName = "NuGet Client V3";
1820

19-
private const string UserAgentWithOSDescriptionAndVisualStudioSKUTemplate = "{0}/{1} ({2}, {3})";
20-
private const string UserAgentWithOSDescriptionTemplate = "{0}/{1} ({2})";
21+
private const string UserAgentWithMetadataTemplate = "{0}/{1} ({2})";
2122
private const string UserAgentTemplate = "{0}/{1}";
2223

2324
private readonly string _clientName;
2425
private string _vsInfo;
2526
private string _osInfo;
27+
private string _ciInfo;
2628

2729
public UserAgentStringBuilder()
2830
: this(DefaultNuGetClientName)
2931
{
3032
}
3133

3234
public UserAgentStringBuilder(string clientName)
35+
: this(clientName, EnvironmentVariableWrapper.Instance)
36+
{
37+
}
38+
39+
/// <summary>
40+
/// Internal constructor for testing purposes that allows injecting an environment variable reader.
41+
/// </summary>
42+
/// <param name="clientName">The client name to use in the user agent string.</param>
43+
/// <param name="environmentVariableReader">The environment variable reader for CI detection.</param>
44+
internal UserAgentStringBuilder(string clientName, IEnvironmentVariableReader environmentVariableReader)
3345
{
3446
_clientName = clientName;
3547

3648
// Read the client version from the assembly metadata and normalize it.
3749
NuGetClientVersion = MinClientVersionUtility.GetNuGetClientVersion().ToNormalizedString();
3850

3951
_osInfo = GetOS();
52+
_ciInfo = CIEnvironmentDetector.Detect(environmentVariableReader);
4053
}
4154

4255
public string NuGetClientVersion { get; }
@@ -59,7 +72,9 @@ public string Build()
5972
clientInfo = DefaultNuGetClientName;
6073
}
6174

62-
if (string.IsNullOrEmpty(_osInfo))
75+
string metadataString = BuildMetadataString();
76+
77+
if (string.IsNullOrEmpty(metadataString))
6378
{
6479
return string.Format(
6580
CultureInfo.InvariantCulture,
@@ -68,25 +83,43 @@ public string Build()
6883
NuGetClientVersion);
6984
}
7085

71-
if (string.IsNullOrEmpty(_vsInfo))
86+
return string.Format(
87+
CultureInfo.InvariantCulture,
88+
UserAgentWithMetadataTemplate,
89+
clientInfo,
90+
NuGetClientVersion,
91+
metadataString);
92+
}
93+
94+
/// <summary>
95+
/// Builds the metadata string for the parentheses section.
96+
/// Items are collected in order (OS, CI, VS) and joined with ", ".
97+
/// </summary>
98+
internal string BuildMetadataString()
99+
{
100+
var sb = new StringBuilder();
101+
102+
// OS info
103+
if (!string.IsNullOrEmpty(_osInfo))
72104
{
73-
return string.Format(
74-
CultureInfo.InvariantCulture,
75-
UserAgentWithOSDescriptionTemplate,
76-
clientInfo,
77-
NuGetClientVersion,
78-
_osInfo);
105+
sb.Append(_osInfo);
79106
}
80-
else
107+
108+
// CI info (formatted as "CI: {provider}")
109+
if (!string.IsNullOrEmpty(_ciInfo))
81110
{
82-
return string.Format(
83-
CultureInfo.InvariantCulture,
84-
UserAgentWithOSDescriptionAndVisualStudioSKUTemplate,
85-
_clientName,
86-
NuGetClientVersion, /* NuGet version */
87-
_osInfo, /* OS version */
88-
_vsInfo); /* VS SKU + version */
111+
if (sb.Length > 0) sb.Append(", ");
112+
sb.Append("CI: ").Append(_ciInfo);
89113
}
114+
115+
// VS info
116+
if (!string.IsNullOrEmpty(_vsInfo))
117+
{
118+
if (sb.Length > 0) sb.Append(", ");
119+
sb.Append(_vsInfo);
120+
}
121+
122+
return sb.ToString();
90123
}
91124

92125
internal static string GetOS()

0 commit comments

Comments
 (0)