@@ -3,11 +3,15 @@ package nuget
33import (
44 "bufio"
55 "context"
6+ "crypto/tls"
67 "encoding/base64"
78 "encoding/json"
9+ "encoding/xml"
810 "errors"
911 "fmt"
1012 "io"
13+ "net/http"
14+ neturl "net/url"
1115 "os"
1216 "os/exec"
1317 "path/filepath"
@@ -33,8 +37,30 @@ var _ErrDotnetNotFound = errors.New("dotnet not found")
3337const (
3438 nugetBuildMaxTimeout = 6 * time .Hour
3539 nugetCommandIdleTimeout = 30 * time .Second
40+ nugetPreflightTimeout = 8 * time .Second
3641)
3742
43+ type nugetConfigXML struct {
44+ PackageSources nugetPackageSourcesXML `xml:"packageSources"`
45+ }
46+
47+ type nugetPackageSourcesXML struct {
48+ Clear * struct {} `xml:"clear"`
49+ Add []nugetSourceAddXML `xml:"add"`
50+ }
51+
52+ type nugetSourceAddXML struct {
53+ Value string `xml:"value,attr"`
54+ }
55+
56+ type nugetSourceProbeResult struct {
57+ Source string
58+ Reachable bool
59+ StatusCode int
60+ Err error
61+ Duration time.Duration
62+ }
63+
3864func tailText (s string , max int ) string {
3965 s = strings .TrimSpace (s )
4066 if s == "" || max <= 0 || len (s ) <= max {
@@ -119,6 +145,153 @@ func logNugetToolVersion(ctx context.Context, logger *zap.Logger) {
119145 logger .Sugar ().Infof ("NuGet detected: path=%s version=%s" , dotnetPath , strings .TrimSpace (string (data )))
120146}
121147
148+ func collectNugetSources (solutionPath string ) ([]string , error ) {
149+ paths := detectNugetConfigPaths (solutionPath )
150+ existing := make ([]string , 0 , len (paths ))
151+ for _ , p := range paths {
152+ if _ , err := os .Stat (p ); err == nil {
153+ existing = append (existing , p )
154+ }
155+ }
156+ // Apply low-priority first, then high-priority overrides.
157+ for i , j := 0 , len (existing )- 1 ; i < j ; i , j = i + 1 , j - 1 {
158+ existing [i ], existing [j ] = existing [j ], existing [i ]
159+ }
160+
161+ sources := make ([]string , 0 )
162+ seen := map [string ]struct {}{}
163+ for _ , p := range existing {
164+ items , hasClear , err := parseNugetSourcesFromConfig (p )
165+ if err != nil {
166+ return nil , fmt .Errorf ("parse NuGet config failed (%s): %w" , p , err )
167+ }
168+ if hasClear {
169+ sources = sources [:0 ]
170+ seen = map [string ]struct {}{}
171+ }
172+ for _ , s := range items {
173+ if _ , ok := seen [s ]; ok {
174+ continue
175+ }
176+ seen [s ] = struct {}{}
177+ sources = append (sources , s )
178+ }
179+ }
180+ return sources , nil
181+ }
182+
183+ func parseNugetSourcesFromConfig (path string ) ([]string , bool , error ) {
184+ data , err := os .ReadFile (path )
185+ if err != nil {
186+ return nil , false , err
187+ }
188+ var cfg nugetConfigXML
189+ if err = xml .Unmarshal (data , & cfg ); err != nil {
190+ return nil , false , err
191+ }
192+ out := make ([]string , 0 , len (cfg .PackageSources .Add ))
193+ for _ , item := range cfg .PackageSources .Add {
194+ v := strings .TrimSpace (item .Value )
195+ if v != "" {
196+ out = append (out , v )
197+ }
198+ }
199+ return out , cfg .PackageSources .Clear != nil , nil
200+ }
201+
202+ func maskURLCredential (raw string ) string {
203+ u , err := neturl .Parse (raw )
204+ if err != nil {
205+ return raw
206+ }
207+ if u .User != nil {
208+ user := u .User .Username ()
209+ if user != "" {
210+ u .User = neturl .UserPassword (user , "******" )
211+ }
212+ }
213+ return u .String ()
214+ }
215+
216+ func probeNugetSource (ctx context.Context , source string ) nugetSourceProbeResult {
217+ start := time .Now ()
218+ res := nugetSourceProbeResult {Source : source }
219+ reqCtx , cancel := context .WithTimeout (ctx , nugetPreflightTimeout )
220+ defer cancel ()
221+
222+ transport := & http.Transport {}
223+ if os .Getenv ("TLS_ALLOW_INSECURE" ) == "1" {
224+ transport .TLSClientConfig = & tls.Config {InsecureSkipVerify : true }
225+ }
226+ client := & http.Client {
227+ Transport : transport ,
228+ Timeout : nugetPreflightTimeout ,
229+ }
230+ req , err := http .NewRequestWithContext (reqCtx , http .MethodGet , source , nil )
231+ if err != nil {
232+ res .Err = err
233+ res .Duration = time .Since (start )
234+ return res
235+ }
236+ resp , err := client .Do (req )
237+ if err != nil {
238+ res .Err = err
239+ res .Duration = time .Since (start )
240+ return res
241+ }
242+ defer resp .Body .Close ()
243+ _ , _ = io .CopyN (io .Discard , resp .Body , 1024 )
244+ res .StatusCode = resp .StatusCode
245+ res .Duration = time .Since (start )
246+ res .Reachable = (resp .StatusCode >= 200 && resp .StatusCode < 400 ) || resp .StatusCode == 401 || resp .StatusCode == 403
247+ return res
248+ }
249+
250+ func preflightNugetSources (ctx context.Context , logger * zap.Logger , solutionPath string ) error {
251+ sources , err := collectNugetSources (solutionPath )
252+ if err != nil {
253+ logger .Sugar ().Warnf ("NuGet source preflight parse failed, skip fast-fail: %v" , err )
254+ return nil
255+ }
256+ if len (sources ) == 0 {
257+ logger .Warn ("NuGet source preflight skipped: no package sources found" )
258+ return nil
259+ }
260+ logger .Sugar ().Infof ("NuGet source preflight start: total=%d" , len (sources ))
261+ results := make ([]nugetSourceProbeResult , 0 , len (sources ))
262+ reachable := 0
263+ for _ , s := range sources {
264+ r := probeNugetSource (ctx , s )
265+ results = append (results , r )
266+ if r .Reachable {
267+ reachable ++
268+ logger .Sugar ().Infof ("NuGet source preflight ok: source=%s status=%d cost=%s" ,
269+ maskURLCredential (s ), r .StatusCode , r .Duration .Round (time .Millisecond ))
270+ continue
271+ }
272+ if r .Err != nil {
273+ logger .Sugar ().Warnf ("NuGet source preflight failed: source=%s err=%v cost=%s" ,
274+ maskURLCredential (s ), r .Err , r .Duration .Round (time .Millisecond ))
275+ } else {
276+ logger .Sugar ().Warnf ("NuGet source preflight failed: source=%s status=%d cost=%s" ,
277+ maskURLCredential (s ), r .StatusCode , r .Duration .Round (time .Millisecond ))
278+ }
279+ }
280+ if reachable > 0 {
281+ logger .Sugar ().Infof ("NuGet source preflight completed: reachable=%d/%d" , reachable , len (sources ))
282+ return nil
283+ }
284+ parts := make ([]string , 0 , len (results ))
285+ for _ , r := range results {
286+ if r .Err != nil {
287+ parts = append (parts , fmt .Sprintf ("%s err=%v" , maskURLCredential (r .Source ), r .Err ))
288+ } else {
289+ parts = append (parts , fmt .Sprintf ("%s status=%d" , maskURLCredential (r .Source ), r .StatusCode ))
290+ }
291+ }
292+ return fmt .Errorf ("NuGet source preflight failed: all sources unreachable (%d). details: %s" , len (results ), strings .Join (parts , "; " ))
293+ }
294+
122295func multipleBuilds (ctx context.Context , task * model.InspectionTask ) error {
123296 logger := logctx .Use (ctx )
124297 slnPaths , err := findCLNList (task .Dir ())
@@ -419,6 +592,9 @@ func listNuget(ctx context.Context, task *model.InspectionTask, solutionPath str
419592 var logger = logctx .Use (ctx )
420593 logNugetToolVersion (ctx , logger )
421594 logNugetRemoteConfigPaths (logger , solutionPath )
595+ if err = preflightNugetSources (ctx , logger , solutionPath ); err != nil {
596+ return err
597+ }
422598 err = buildPackage (ctx , logger , solutionPath )
423599 if err != nil {
424600 return
0 commit comments