1+ using  System . Net . Http . Headers ; 
2+ using  System . Text . Json ; 
13using  Cake . Common . Tools . DotNet . NuGet . Push ; 
24using  Common . Utilities ; 
35
@@ -10,18 +12,21 @@ 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    { 
1719        var  shouldRun  =  true ; 
18-         shouldRun  &=  context . ShouldRun ( context . IsGitHubActionsBuild ,  $ "{ nameof ( PublishNuget ) }  works only on GitHub Actions.") ; 
19-         shouldRun  &=  context . ShouldRun ( context . IsStableRelease  ||  context . IsTaggedPreRelease  ||  context . IsInternalPreRelease ,  $ "{ nameof ( PublishNuget ) }  works only for releases.") ; 
20+         shouldRun  &=  context . ShouldRun ( context . IsGitHubActionsBuild , 
21+             $ "{ nameof ( PublishNuget ) }  works only on GitHub Actions.") ; 
22+         shouldRun  &= 
23+             context . ShouldRun ( context . IsStableRelease  ||  context . IsTaggedPreRelease  ||  context . IsInternalPreRelease , 
24+                 $ "{ nameof ( PublishNuget ) }  works only for releases.") ; 
2025
2126        return  shouldRun ; 
2227    } 
2328
24-     public  override  void   Run ( BuildContext  context ) 
29+     public  override  async   Task   RunAsync ( BuildContext  context ) 
2530    { 
2631        // publish to github packages for commits on main and on original repo 
2732        if  ( context . IsInternalPreRelease ) 
@@ -32,35 +37,132 @@ public override void Run(BuildContext context)
3237            { 
3338                throw  new  InvalidOperationException ( "Could not resolve NuGet GitHub Packages API key." ) ; 
3439            } 
40+ 
3541            PublishToNugetRepo ( context ,  apiKey ,  Constants . GithubPackagesUrl ) ; 
3642            context . EndGroup ( ) ; 
3743        } 
44+ 
45+         var  nugetApiKey  =  await  GetNugetApiKey ( context ) ; 
3846        // publish to nuget.org for tagged releases 
3947        if  ( context . IsStableRelease  ||  context . IsTaggedPreRelease ) 
4048        { 
4149            context . StartGroup ( "Publishing to Nuget.org" ) ; 
42-             var  apiKey  =  context . Credentials ? . Nuget ? . ApiKey ; 
43-             if  ( string . IsNullOrEmpty ( apiKey ) ) 
50+             if  ( string . IsNullOrEmpty ( nugetApiKey ) ) 
4451            { 
4552                throw  new  InvalidOperationException ( "Could not resolve NuGet org API key." ) ; 
4653            } 
47-             PublishToNugetRepo ( context ,  apiKey ,  Constants . NugetOrgUrl ) ; 
54+ 
55+             PublishToNugetRepo ( context ,  nugetApiKey ,  Constants . NugetOrgUrl ) ; 
4856            context . EndGroup ( ) ; 
4957        } 
5058    } 
59+ 
5160    private  static   void  PublishToNugetRepo ( BuildContext  context ,  string  apiKey ,  string  apiUrl ) 
5261    { 
5362        ArgumentNullException . ThrowIfNull ( context . Version ) ; 
5463        var  nugetVersion  =  context . Version . NugetVersion ; 
5564        foreach  ( var  ( packageName ,  filePath ,  _)  in  context . Packages . Where ( x =>  ! x . IsChocoPackage ) ) 
5665        { 
5766            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-             } ) ; 
67+             context . DotNetNuGetPush ( filePath . FullPath , 
68+                 new  DotNetNuGetPushSettings  {  ApiKey  =  apiKey ,  Source  =  apiUrl ,  SkipDuplicate  =  true  } ) ; 
69+         } 
70+     } 
71+ 
72+     private  static   async  Task < string ? >  GetNugetApiKey ( BuildContext  context ) 
73+     { 
74+         try 
75+         { 
76+             var  oidcToken  =  await  GetGitHubOidcToken ( context ) ; 
77+             var  apiKey  =  await  ExchangeOidcTokenForApiKey ( oidcToken ) ; 
78+ 
79+             context . Information ( $ "Successfully exchanged OIDC token for NuGet API key.") ; 
80+             return  apiKey ; 
81+         } 
82+         catch  ( Exception  ex ) 
83+         { 
84+             context . Error ( $ "Failed to retrieve NuGet API key: { ex . Message } ") ; 
85+             return  null ; 
86+         } 
87+     } 
88+ 
89+     private  static   async  Task < string >  GetGitHubOidcToken ( BuildContext  context ) 
90+     { 
91+         const  string  nugetAudience  =  "https://www.nuget.org" ; 
92+ 
93+         var  oidcRequestToken  =  context . Environment . GetEnvironmentVariable ( "ACTIONS_ID_TOKEN_REQUEST_TOKEN" ) ; 
94+         var  oidcRequestUrl  =  context . Environment . GetEnvironmentVariable ( "ACTIONS_ID_TOKEN_REQUEST_URL" ) ; 
95+ 
96+         if  ( string . IsNullOrEmpty ( oidcRequestToken )  ||  string . IsNullOrEmpty ( oidcRequestUrl ) ) 
97+             throw  new  InvalidOperationException ( "Missing GitHub OIDC request environment variables." ) ; 
98+ 
99+         var  tokenUrl  =  $ "{ oidcRequestUrl } &audience={ Uri . EscapeDataString ( nugetAudience ) } "; 
100+         context . Information ( $ "Requesting GitHub OIDC token from: { tokenUrl } ") ; 
101+ 
102+         using  var  http  =  new  HttpClient ( ) ; 
103+         http . DefaultRequestHeaders . Authorization  =  new  AuthenticationHeaderValue ( "Bearer" ,  oidcRequestToken ) ; 
104+ 
105+         var  responseMessage  =  await  http . GetAsync ( tokenUrl ) ; 
106+         var  tokenBody  =  await  responseMessage . Content . ReadAsStringAsync ( ) ; 
107+ 
108+         if  ( ! responseMessage . IsSuccessStatusCode ) 
109+             throw  new  Exception ( "Failed to retrieve OIDC token from GitHub." ) ; 
110+ 
111+         using  var  tokenDoc  =  JsonDocument . Parse ( tokenBody ) ; 
112+         return  ParseJsonProperty ( tokenDoc ,  "value" ,  "Failed to retrieve OIDC token from GitHub." ) ; 
113+     } 
114+ 
115+     private  static   async  Task < string >  ExchangeOidcTokenForApiKey ( string  oidcToken ) 
116+     { 
117+         const  string  nugetUsername  =  "gittoolsbot" ; 
118+         const  string  nugetTokenServiceUrl  =  "https://www.nuget.org/api/v2/token" ; 
119+ 
120+         var  requestBody  =  JsonSerializer . Serialize ( new  {  username  =  nugetUsername ,  tokenType  =  "ApiKey"  } ) ; 
121+ 
122+         using  var  tokenServiceHttp  =  new  HttpClient ( ) ; 
123+         tokenServiceHttp . DefaultRequestHeaders . Authorization  =  new  AuthenticationHeaderValue ( "Bearer" ,  oidcToken ) ; 
124+         tokenServiceHttp . DefaultRequestHeaders . UserAgent . ParseAdd ( "nuget/login-action" ) ; 
125+         var  content  =  new  StringContent ( requestBody ,  Encoding . UTF8 ,  "application/json" ) ; 
126+ 
127+         var  responseMessage  =  await  tokenServiceHttp . PostAsync ( nugetTokenServiceUrl ,  content ) ; 
128+         var  exchangeBody  =  await  responseMessage . Content . ReadAsStringAsync ( ) ; 
129+ 
130+         if  ( ! responseMessage . IsSuccessStatusCode ) 
131+         { 
132+             var  errorMessage  =  BuildErrorMessage ( ( int ) responseMessage . StatusCode ,  exchangeBody ) ; 
133+             throw  new  Exception ( errorMessage ) ; 
64134        } 
135+ 
136+         using  var  respDoc  =  JsonDocument . Parse ( exchangeBody ) ; 
137+         return  ParseJsonProperty ( respDoc ,  "apiKey" ,  "Response did not contain \" apiKey\" ." ) ; 
138+     } 
139+ 
140+     private  static   string  ParseJsonProperty ( JsonDocument  document ,  string  propertyName ,  string  errorMessage ) 
141+     { 
142+         if  ( ! document . RootElement . TryGetProperty ( propertyName ,  out  var  property )  || 
143+             property . ValueKind  !=  JsonValueKind . String ) 
144+             throw  new  Exception ( errorMessage ) ; 
145+ 
146+         return  property . GetString ( )  ??  throw  new  Exception ( errorMessage ) ; 
147+     } 
148+ 
149+     private  static   string  BuildErrorMessage ( int  statusCode ,  string  responseBody ) 
150+     { 
151+         var  errorMessage  =  $ "Token exchange failed ({ statusCode } )"; 
152+         try 
153+         { 
154+             using  var  errDoc  =  JsonDocument . Parse ( responseBody ) ; 
155+             errorMessage  += 
156+                 errDoc . RootElement . TryGetProperty ( "error" ,  out  var  errProp )  && 
157+                 errProp . ValueKind  ==  JsonValueKind . String 
158+                     ?  $ ": { errProp . GetString ( ) } "
159+                     :  $ ": { responseBody } "; 
160+         } 
161+         catch 
162+         { 
163+             errorMessage  +=  $ ": { responseBody } "; 
164+         } 
165+ 
166+         return  errorMessage ; 
65167    } 
66168} 
0 commit comments