1+ using System . Net . Http . Headers ;
2+ using System . Text . Json ;
13using Cake . Common . Tools . DotNet . NuGet . Push ;
24using Common . Utilities ;
35
@@ -10,7 +12,7 @@ public class PublishNuget : FrostingTask<BuildContext>;
1012
1113[ TaskName ( nameof ( PublishNugetInternal ) ) ]
1214[ TaskDescription ( "Publish nuget packages" ) ]
13- public class PublishNugetInternal : FrostingTask < BuildContext >
15+ public class PublishNugetInternal : AsyncFrostingTask < BuildContext >
1416{
1517 public override bool ShouldRun ( BuildContext context )
1618 {
@@ -21,7 +23,7 @@ public override bool ShouldRun(BuildContext context)
2123 return shouldRun ;
2224 }
2325
24- public override void Run ( BuildContext context )
26+ public override async Task RunAsync ( BuildContext context )
2527 {
2628 // publish to github packages for commits on main and on original repo
2729 if ( context . IsInternalPreRelease )
@@ -32,35 +34,147 @@ public override void Run(BuildContext context)
3234 {
3335 throw new InvalidOperationException ( "Could not resolve NuGet GitHub Packages API key." ) ;
3436 }
37+
3538 PublishToNugetRepo ( context , apiKey , Constants . GithubPackagesUrl ) ;
3639 context . EndGroup ( ) ;
3740 }
41+
3842 // publish to nuget.org for tagged releases
3943 if ( context . IsStableRelease || context . IsTaggedPreRelease )
4044 {
4145 context . StartGroup ( "Publishing to Nuget.org" ) ;
42- var apiKey = context . Credentials ? . Nuget ? . ApiKey ;
46+ var apiKey = await GetNugetApiKey ( context ) ;
4347 if ( string . IsNullOrEmpty ( apiKey ) )
4448 {
4549 throw new InvalidOperationException ( "Could not resolve NuGet org API key." ) ;
4650 }
51+
4752 PublishToNugetRepo ( context , apiKey , Constants . NugetOrgUrl ) ;
4853 context . EndGroup ( ) ;
4954 }
5055 }
56+
5157 private static void PublishToNugetRepo ( BuildContext context , string apiKey , string apiUrl )
5258 {
5359 ArgumentNullException . ThrowIfNull ( context . Version ) ;
5460 var nugetVersion = context . Version . NugetVersion ;
5561 foreach ( var ( packageName , filePath , _) in context . Packages . Where ( x => ! x . IsChocoPackage ) )
5662 {
5763 context . Information ( $ "Package { packageName } , version { nugetVersion } is being published.") ;
58- context . DotNetNuGetPush ( filePath . FullPath , new DotNetNuGetPushSettings
59- {
60- ApiKey = apiKey ,
61- Source = apiUrl ,
62- SkipDuplicate = true
63- } ) ;
64+ context . DotNetNuGetPush ( filePath . FullPath ,
65+ new DotNetNuGetPushSettings
66+ {
67+ ApiKey = apiKey ,
68+ Source = apiUrl ,
69+ SkipDuplicate = true
70+ } ) ;
71+ }
72+ }
73+
74+ private static async Task < string ? > GetNugetApiKey ( BuildContext context )
75+ {
76+ try
77+ {
78+ var oidcToken = await GetGitHubOidcToken ( context ) ;
79+ var apiKey = await ExchangeOidcTokenForApiKey ( oidcToken ) ;
80+
81+ context . Information ( $ "Successfully exchanged OIDC token for NuGet API key.") ;
82+ return apiKey ;
83+ }
84+ catch ( HttpRequestException ex )
85+ {
86+ context . Error ( $ "Network error while retrieving NuGet API key: { ex . Message } ") ;
87+ return null ;
6488 }
89+ catch ( InvalidOperationException ex )
90+ {
91+ context . Error ( $ "Invalid operation while retrieving NuGet API key: { ex . Message } ") ;
92+ return null ;
93+ }
94+ catch ( JsonException ex )
95+ {
96+ context . Error ( $ "JSON parsing error while retrieving NuGet API key: { ex . Message } ") ;
97+ return null ;
98+ }
99+ }
100+
101+ private static async Task < string > GetGitHubOidcToken ( BuildContext context )
102+ {
103+ const string nugetAudience = "https://www.nuget.org" ;
104+
105+ var oidcRequestToken = context . Environment . GetEnvironmentVariable ( "ACTIONS_ID_TOKEN_REQUEST_TOKEN" ) ;
106+ var oidcRequestUrl = context . Environment . GetEnvironmentVariable ( "ACTIONS_ID_TOKEN_REQUEST_URL" ) ;
107+
108+ if ( string . IsNullOrEmpty ( oidcRequestToken ) || string . IsNullOrEmpty ( oidcRequestUrl ) )
109+ throw new InvalidOperationException ( "Missing GitHub OIDC request environment variables." ) ;
110+
111+ var tokenUrl = $ "{ oidcRequestUrl } &audience={ Uri . EscapeDataString ( nugetAudience ) } ";
112+ context . Information ( $ "Requesting GitHub OIDC token from: { tokenUrl } ") ;
113+
114+ using var http = new HttpClient ( ) ;
115+ http . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , oidcRequestToken ) ;
116+
117+ var responseMessage = await http . GetAsync ( tokenUrl ) ;
118+ var tokenBody = await responseMessage . Content . ReadAsStringAsync ( ) ;
119+
120+ if ( ! responseMessage . IsSuccessStatusCode )
121+ throw new Exception ( "Failed to retrieve OIDC token from GitHub." ) ;
122+
123+ using var tokenDoc = JsonDocument . Parse ( tokenBody ) ;
124+ return ParseJsonProperty ( tokenDoc , "value" , "Failed to retrieve OIDC token from GitHub." ) ;
125+ }
126+
127+ private static async Task < string > ExchangeOidcTokenForApiKey ( string oidcToken )
128+ {
129+ const string nugetUsername = "gittoolsbot" ;
130+ const string nugetTokenServiceUrl = "https://www.nuget.org/api/v2/token" ;
131+
132+ var requestBody = JsonSerializer . Serialize ( new { username = nugetUsername , tokenType = "ApiKey" } ) ;
133+
134+ using var tokenServiceHttp = new HttpClient ( ) ;
135+ tokenServiceHttp . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , oidcToken ) ;
136+ tokenServiceHttp . DefaultRequestHeaders . UserAgent . ParseAdd ( "nuget/login-action" ) ;
137+ using var content = new StringContent ( requestBody , Encoding . UTF8 , "application/json" ) ;
138+
139+ var responseMessage = await tokenServiceHttp . PostAsync ( nugetTokenServiceUrl , content ) ;
140+ var exchangeBody = await responseMessage . Content . ReadAsStringAsync ( ) ;
141+
142+ if ( ! responseMessage . IsSuccessStatusCode )
143+ {
144+ var errorMessage = BuildErrorMessage ( ( int ) responseMessage . StatusCode , exchangeBody ) ;
145+ throw new Exception ( errorMessage ) ;
146+ }
147+
148+ using var respDoc = JsonDocument . Parse ( exchangeBody ) ;
149+ return ParseJsonProperty ( respDoc , "apiKey" , "Response did not contain \" apiKey\" ." ) ;
150+ }
151+
152+ private static string ParseJsonProperty ( JsonDocument document , string propertyName , string errorMessage )
153+ {
154+ if ( ! document . RootElement . TryGetProperty ( propertyName , out var property ) ||
155+ property . ValueKind != JsonValueKind . String )
156+ throw new Exception ( errorMessage ) ;
157+
158+ return property . GetString ( ) ?? throw new Exception ( errorMessage ) ;
159+ }
160+
161+ private static string BuildErrorMessage ( int statusCode , string responseBody )
162+ {
163+ var errorMessage = $ "Token exchange failed ({ statusCode } )";
164+ try
165+ {
166+ using var errDoc = JsonDocument . Parse ( responseBody ) ;
167+ errorMessage +=
168+ errDoc . RootElement . TryGetProperty ( "error" , out var errProp ) &&
169+ errProp . ValueKind == JsonValueKind . String
170+ ? $ ": { errProp . GetString ( ) } "
171+ : $ ": { responseBody } ";
172+ }
173+ catch ( Exception )
174+ {
175+ errorMessage += $ ": { responseBody } ";
176+ }
177+
178+ return errorMessage ;
65179 }
66180}
0 commit comments