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,142 @@ 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 { ApiKey = apiKey , Source = apiUrl , SkipDuplicate = true } ) ;
66+ }
67+ }
68+
69+ private static async Task < string ? > GetNugetApiKey ( BuildContext context )
70+ {
71+ try
72+ {
73+ var oidcToken = await GetGitHubOidcToken ( context ) ;
74+ var apiKey = await ExchangeOidcTokenForApiKey ( oidcToken ) ;
75+
76+ context . Information ( $ "Successfully exchanged OIDC token for NuGet API key.") ;
77+ return apiKey ;
78+ }
79+ catch ( HttpRequestException ex )
80+ {
81+ context . Error ( $ "Network error while retrieving NuGet API key: { ex . Message } ") ;
82+ return null ;
6483 }
84+ catch ( InvalidOperationException ex )
85+ {
86+ context . Error ( $ "Invalid operation while retrieving NuGet API key: { ex . Message } ") ;
87+ return null ;
88+ }
89+ catch ( JsonException ex )
90+ {
91+ context . Error ( $ "JSON parsing error while retrieving NuGet API key: { ex . Message } ") ;
92+ return null ;
93+ }
94+ }
95+
96+ private static async Task < string > GetGitHubOidcToken ( BuildContext context )
97+ {
98+ const string nugetAudience = "https://www.nuget.org" ;
99+
100+ var oidcRequestToken = context . Environment . GetEnvironmentVariable ( "ACTIONS_ID_TOKEN_REQUEST_TOKEN" ) ;
101+ var oidcRequestUrl = context . Environment . GetEnvironmentVariable ( "ACTIONS_ID_TOKEN_REQUEST_URL" ) ;
102+
103+ if ( string . IsNullOrEmpty ( oidcRequestToken ) || string . IsNullOrEmpty ( oidcRequestUrl ) )
104+ throw new InvalidOperationException ( "Missing GitHub OIDC request environment variables." ) ;
105+
106+ var tokenUrl = $ "{ oidcRequestUrl } &audience={ Uri . EscapeDataString ( nugetAudience ) } ";
107+ context . Information ( $ "Requesting GitHub OIDC token from: { tokenUrl } ") ;
108+
109+ using var http = new HttpClient ( ) ;
110+ http . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , oidcRequestToken ) ;
111+
112+ var responseMessage = await http . GetAsync ( tokenUrl ) ;
113+ var tokenBody = await responseMessage . Content . ReadAsStringAsync ( ) ;
114+
115+ if ( ! responseMessage . IsSuccessStatusCode )
116+ throw new Exception ( "Failed to retrieve OIDC token from GitHub." ) ;
117+
118+ using var tokenDoc = JsonDocument . Parse ( tokenBody ) ;
119+ return ParseJsonProperty ( tokenDoc , "value" , "Failed to retrieve OIDC token from GitHub." ) ;
120+ }
121+
122+ private static async Task < string > ExchangeOidcTokenForApiKey ( string oidcToken )
123+ {
124+ const string nugetUsername = "gittoolsbot" ;
125+ const string nugetTokenServiceUrl = "https://www.nuget.org/api/v2/token" ;
126+
127+ var requestBody = JsonSerializer . Serialize ( new { username = nugetUsername , tokenType = "ApiKey" } ) ;
128+
129+ using var tokenServiceHttp = new HttpClient ( ) ;
130+ tokenServiceHttp . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , oidcToken ) ;
131+ tokenServiceHttp . DefaultRequestHeaders . UserAgent . ParseAdd ( "nuget/login-action" ) ;
132+ using var content = new StringContent ( requestBody , Encoding . UTF8 , "application/json" ) ;
133+
134+ var responseMessage = await tokenServiceHttp . PostAsync ( nugetTokenServiceUrl , content ) ;
135+ var exchangeBody = await responseMessage . Content . ReadAsStringAsync ( ) ;
136+
137+ if ( ! responseMessage . IsSuccessStatusCode )
138+ {
139+ var errorMessage = BuildErrorMessage ( ( int ) responseMessage . StatusCode , exchangeBody ) ;
140+ throw new Exception ( errorMessage ) ;
141+ }
142+
143+ using var respDoc = JsonDocument . Parse ( exchangeBody ) ;
144+ return ParseJsonProperty ( respDoc , "apiKey" , "Response did not contain \" apiKey\" ." ) ;
145+ }
146+
147+ private static string ParseJsonProperty ( JsonDocument document , string propertyName , string errorMessage )
148+ {
149+ if ( ! document . RootElement . TryGetProperty ( propertyName , out var property ) ||
150+ property . ValueKind != JsonValueKind . String )
151+ throw new Exception ( errorMessage ) ;
152+
153+ return property . GetString ( ) ?? throw new Exception ( errorMessage ) ;
154+ }
155+
156+ private static string BuildErrorMessage ( int statusCode , string responseBody )
157+ {
158+ var errorMessage = $ "Token exchange failed ({ statusCode } )";
159+ try
160+ {
161+ using var errDoc = JsonDocument . Parse ( responseBody ) ;
162+ errorMessage +=
163+ errDoc . RootElement . TryGetProperty ( "error" , out var errProp ) &&
164+ errProp . ValueKind == JsonValueKind . String
165+ ? $ ": { errProp . GetString ( ) } "
166+ : $ ": { responseBody } ";
167+ }
168+ catch ( Exception )
169+ {
170+ errorMessage += $ ": { responseBody } ";
171+ }
172+
173+ return errorMessage ;
65174 }
66175}
0 commit comments