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