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