@@ -9,6 +9,7 @@ import fs = require("fs");
9
9
import path = require( "path" ) ;
10
10
import mdEscape = require( "markdown-escape" ) ;
11
11
import randomSeed = require( "random-seed" ) ;
12
+ import { getErrorMessageFromStack , getHash , getHashForStack } from "./utils/hashStackTrace" ;
12
13
13
14
interface Params {
14
15
/**
@@ -84,9 +85,31 @@ export type RepoStatus =
84
85
| "Detected no interesting changes"
85
86
;
86
87
88
+ interface TSServerResult {
89
+ oldServerFailed : boolean ;
90
+ oldSpawnResult ?: SpawnResult ;
91
+ newServerFailed : boolean ;
92
+ newSpawnResult : SpawnResult ;
93
+ replayScriptPath : string ;
94
+ installCommands : ip . InstallCommand [ ] ;
95
+ }
96
+
97
+ interface Summary {
98
+ tsServerResult : TSServerResult ;
99
+ repo : git . Repo ;
100
+ oldTsEntrypointPath : string ;
101
+ rawErrorArtifactPath : string ;
102
+ replayScript : string ;
103
+ downloadDir : string ;
104
+ replayScriptArtifactPath : string ;
105
+ replayScriptName : string ;
106
+ commit ?: string ;
107
+ }
108
+
87
109
interface RepoResult {
88
110
readonly status : RepoStatus ;
89
111
readonly summary ?: string ;
112
+ readonly tsServerResult ?: TSServerResult ;
90
113
readonly replayScriptPath ?: string ;
91
114
readonly rawErrorPath ?: string ;
92
115
}
@@ -203,8 +226,6 @@ async function getTsServerRepoResult(
203
226
? ( await installPackagesAndGetCommands ( repo , downloadDir , repoDir , monorepoPackages , /*cleanOnFailure*/ true , diagnosticOutput ) ) !
204
227
: [ ] ;
205
228
206
- const isUserTestRepo = ! repo . url ;
207
-
208
229
const replayScriptName = path . basename ( replayScriptArtifactPath ) ;
209
230
const replayScriptPath = path . join ( downloadDir , replayScriptName ) ;
210
231
@@ -302,110 +323,169 @@ async function getTsServerRepoResult(
302
323
}
303
324
}
304
325
305
- const owner = repo . owner ? `${ mdEscape ( repo . owner ) } /` : "" ;
306
- const url = repo . url ? `(${ repo . url } )` : "" ;
326
+ const tsServerResult = {
327
+ oldServerFailed,
328
+ oldSpawnResult,
329
+ newServerFailed,
330
+ newSpawnResult,
331
+ replayScriptPath,
332
+ installCommands,
333
+ } ;
307
334
308
- let summary = `## [${ owner } ${ mdEscape ( repo . name ) } ]${ url } \n` ;
335
+ if ( oldServerFailed && ! newServerFailed ) {
336
+ return { status : "Detected interesting changes" , tsServerResult }
337
+ }
338
+ if ( ! newServerFailed ) {
339
+ return { status : "Detected no interesting changes" } ;
340
+ }
309
341
310
- if ( oldServerFailed ) {
311
- const oldServerError = oldSpawnResult ?. stdout
312
- ? prettyPrintServerHarnessOutput ( oldSpawnResult . stdout , /*filter*/ true )
342
+ return { status : "Detected interesting changes" , tsServerResult, replayScriptPath, rawErrorPath } ;
343
+ }
344
+ catch ( err ) {
345
+ reportError ( err , `Error running tsserver on ${ repo . url ?? repo . name } ` ) ;
346
+ return { status : "Unknown failure" } ;
347
+ }
348
+ finally {
349
+ console . log ( `Done ${ repo . url ?? repo . name } ` ) ;
350
+ logStepTime ( diagnosticOutput , repo , "language service" , lsStart ) ;
351
+ }
352
+ }
353
+
354
+ function groupErrors ( summaries : Summary [ ] ) {
355
+ const groupedOldErrors = new Map < string , Summary [ ] > ( ) ;
356
+ const groupedNewErrors = new Map < string , Summary [ ] > ( ) ;
357
+ let group : Map < string , Summary [ ] > ;
358
+ let error : ServerHarnessOutput | string ;
359
+ for ( const summary of summaries ) {
360
+ if ( summary . tsServerResult . newServerFailed ) {
361
+ // Group new errors
362
+ error = parseServerHarnessOutput ( summary . tsServerResult . newSpawnResult ! . stdout ) ;
363
+ group = groupedNewErrors ;
364
+ }
365
+ else if ( summary . tsServerResult . oldServerFailed ) {
366
+ // Group old errors
367
+ const { oldSpawnResult } = summary . tsServerResult ;
368
+ error = oldSpawnResult ?. stdout
369
+ ? parseServerHarnessOutput ( oldSpawnResult . stdout )
313
370
: `Timed out after ${ executionTimeout } ms` ;
314
- summary += `
371
+
372
+ group = groupedOldErrors ;
373
+ }
374
+ else {
375
+ continue ;
376
+ }
377
+
378
+ const key = typeof error === "string" ? getHash ( [ error ] ) : getHashForStack ( error . message ) ;
379
+ const value = group . get ( key ) ?? [ ] ;
380
+ value . push ( summary ) ;
381
+ group . set ( key , value ) ;
382
+ }
383
+
384
+ return { groupedOldErrors, groupedNewErrors }
385
+ }
386
+
387
+ function getErrorMessage ( output : string ) : string {
388
+ const error = parseServerHarnessOutput ( output ) ;
389
+
390
+ return typeof error === "string" ? error : getErrorMessageFromStack ( error . message ) ;
391
+ }
392
+
393
+ function createOldErrorSummary ( summaries : Summary [ ] ) : string {
394
+ const { oldSpawnResult } = summaries [ 0 ] . tsServerResult ;
395
+
396
+ const oldServerError = oldSpawnResult ?. stdout
397
+ ? prettyPrintServerHarnessOutput ( oldSpawnResult . stdout , /*filter*/ true )
398
+ : `Timed out after ${ executionTimeout } ms` ;
399
+
400
+ const errorMessage = oldSpawnResult ?. stdout ? getErrorMessage ( oldSpawnResult . stdout ) : oldServerError ;
401
+
402
+ let text = `
315
403
<details>
316
- <summary>:warning: Note that ${ path . basename ( path . dirname ( path . dirname ( oldTsServerPath ) ) ) } had errors :warning: </summary>
404
+ <summary>${ errorMessage } </summary>
317
405
318
406
\`\`\`
319
407
${ oldServerError }
320
408
\`\`\`
321
409
322
- </details >
323
-
410
+ <h4>Repos no longer reporting the error</h4 >
411
+ <ul>
324
412
` ;
325
- if ( ! newServerFailed ) {
326
- summary += `
327
- :tada: New server no longer has errors :tada:
413
+
414
+ for ( const summary of summaries ) {
415
+ const owner = summary . repo . owner ? `${ mdEscape ( summary . repo . owner ) } /` : "" ;
416
+ const url = summary . repo . url ?? "" ;
417
+
418
+ text += `<li><a href="${ url } ">${ owner + mdEscape ( summary . repo . name ) } </a></li>\n`
419
+ }
420
+
421
+ text += `
422
+ </ul>
423
+ </details>
328
424
` ;
329
- return { status : "Detected interesting changes" , summary }
330
- }
331
- }
332
-
333
- if ( ! newServerFailed ) {
334
- return { status : "Detected no interesting changes" } ;
335
- }
336
425
337
- summary += `
426
+ return text ;
427
+ }
428
+
429
+ async function createNewErrorSummaryAsync ( summaries : Summary [ ] ) : Promise < string > {
430
+ let text = `<h2>${ getErrorMessage ( summaries [ 0 ] . tsServerResult . newSpawnResult . stdout ) } </h2>
338
431
339
432
\`\`\`
340
- ${ prettyPrintServerHarnessOutput ( newSpawnResult . stdout , /*filter*/ true ) }
433
+ ${ prettyPrintServerHarnessOutput ( summaries [ 0 ] . tsServerResult . newSpawnResult . stdout , /*filter*/ true ) }
341
434
\`\`\`
342
- That is a filtered view of the text. To see the raw error text, go to ${ rawErrorArtifactPath } </code> in the <a href="${ artifactFolderUrlPlaceholder } ">artifact folder</a></li>\n
343
- ` ;
344
435
345
- summary += `
436
+ <h4>Affected repos</h4>` ;
437
+
438
+ for ( const summary of summaries ) {
439
+ const owner = summary . repo . owner ? `${ mdEscape ( summary . repo . owner ) } /` : "" ;
440
+ const url = summary . repo . url ?? "" ;
441
+
442
+ text += `
346
443
<details>
347
- <summary><h3>Last few requests</h3></summary>
444
+ <summary><a href="${ url } ">${ owner + mdEscape ( summary . repo . name ) } </a></summary>
445
+ Raw error text: <code>${ summary . rawErrorArtifactPath } </code> in the <a href="${ artifactFolderUrlPlaceholder } ">artifact folder</a>
446
+ <h4>Last few requests</h4>
348
447
349
448
\`\`\`json
350
- ${ fs . readFileSync ( replayScriptPath , { encoding : "utf-8" } ) . split ( / \r ? \n / ) . slice ( - 5 ) . join ( "\n" ) }
449
+ ${ summary . replayScript }
351
450
\`\`\`
352
451
353
- </details>
354
-
355
- ` ;
356
-
357
- // Markdown doesn't seem to support a <details> list item, so this chunk is in HTML
358
-
359
- summary += `<details>
360
- <summary><h3>Repro Steps</h3></summary>
452
+ <h4>Repro steps</h4>
361
453
<ol>
362
454
` ;
363
- if ( isUserTestRepo ) {
364
- summary += `<li>Download user test <code>${ repo . name } </code></li>\n` ;
455
+ // No url means is user test repo
456
+ if ( ! summary . repo . url ) {
457
+ text += `<li>Download user test <code>${ summary . repo . name } </code></li>\n` ;
365
458
}
366
459
else {
367
- summary += `<li><code>git clone ${ repo . url ! } --recurse-submodules</code></li>\n` ;
460
+ text += `<li><code>git clone ${ summary . repo . url } --recurse-submodules</code></li>\n` ;
368
461
369
- try {
370
- console . log ( "Extracting commit SHA for repro steps" ) ;
371
- const commit = ( await execAsync ( repoDir , `git rev-parse @` ) ) . trim ( ) ;
372
- summary += `<li>In dir <code>${ repo . name } </code>, run <code>git reset --hard ${ commit } </code></li>\n` ;
373
- }
374
- catch {
462
+ if ( summary . commit ) {
463
+ text += `<li>In dir <code>${ summary . repo . name } </code>, run <code>git reset --hard ${ summary . commit } </code></li>\n` ;
375
464
}
376
465
}
377
466
378
- if ( installCommands . length > 1 ) {
379
- summary += "<li><details><summary>Install packages (exact steps are below, but it might be easier to follow the repo readme)</summary><ol>\n" ;
467
+ if ( summary . tsServerResult . installCommands . length > 1 ) {
468
+ text += "<li><details><summary>Install packages (exact steps are below, but it might be easier to follow the repo readme)</summary><ol>\n" ;
380
469
}
381
- for ( const command of installCommands ) {
382
- summary += ` <li>In dir <code>${ path . relative ( downloadDir , command . directory ) } </code>, run <code>${ command . tool } ${ command . arguments . join ( " " ) } </code></li>\n` ;
470
+ for ( const command of summary . tsServerResult . installCommands ) {
471
+ text += ` <li>In dir <code>${ path . relative ( summary . downloadDir , command . directory ) } </code>, run <code>${ command . tool } ${ command . arguments . join ( " " ) } </code></li>\n` ;
383
472
}
384
- if ( installCommands . length > 1 ) {
385
- summary += "</ol></details>\n" ;
473
+ if ( summary . tsServerResult . installCommands . length > 1 ) {
474
+ text += "</ol></details>\n" ;
386
475
}
387
476
388
477
// The URL of the artifact can be determined via AzDO REST APIs, but not until after the artifact is published
389
- summary += `<li>Back in the initial folder, download <code>${ replayScriptArtifactPath } </code> from the <a href="${ artifactFolderUrlPlaceholder } ">artifact folder</a></li>\n` ;
390
- summary += `<li><code>npm install --no-save @typescript/server-replay</code></li>\n` ;
391
- summary += `<li><code>npx tsreplay ./${ repo . name } ./${ replayScriptName } path/to/tsserver.js</code></li>\n` ;
392
- summary += `<li><code>npx tsreplay --help</code> to learn about helpful switches for debugging, logging, etc</li>\n` ;
478
+ text += `<li>Back in the initial folder, download <code>${ summary . replayScriptArtifactPath } </code> from the <a href="${ artifactFolderUrlPlaceholder } ">artifact folder</a></li>\n` ;
479
+ text += `<li><code>npm install --no-save @typescript/server-replay</code></li>\n` ;
480
+ text += `<li><code>npx tsreplay ./${ summary . repo . name } ./${ summary . replayScriptName } path/to/tsserver.js</code></li>\n` ;
481
+ text += `<li><code>npx tsreplay --help</code> to learn about helpful switches for debugging, logging, etc</li>\n` ;
393
482
394
- summary += `</ol>
483
+ text += `</ol>
395
484
</details>
396
-
397
485
` ;
398
-
399
- return { status : "Detected interesting changes" , summary, replayScriptPath, rawErrorPath } ;
400
- }
401
- catch ( err ) {
402
- reportError ( err , `Error running tsserver on ${ repo . url ?? repo . name } ` ) ;
403
- return { status : "Unknown failure" } ;
404
- }
405
- finally {
406
- console . log ( `Done ${ repo . url ?? repo . name } ` ) ;
407
- logStepTime ( diagnosticOutput , repo , "language service" , lsStart ) ;
408
486
}
487
+
488
+ return text ;
409
489
}
410
490
411
491
// Exported for testing
@@ -658,6 +738,8 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
658
738
659
739
const isPr = params . testType === "user" && ! ! params . prNumber
660
740
741
+ var summaries : Summary [ ] = [ ] ;
742
+
661
743
let i = 1 ;
662
744
for ( const repo of repos ) {
663
745
console . log ( `Starting #${ i ++ } / ${ repos . length } : ${ repo . url ?? repo . name } ` ) ;
@@ -672,16 +754,48 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
672
754
: repo . name ;
673
755
const replayScriptFileName = `${ repoPrefix } .${ replayScriptFileNameSuffix } ` ;
674
756
const rawErrorFileName = `${ repoPrefix } .${ rawErrorFileNameSuffix } ` ;
675
- const { status, summary, replayScriptPath, rawErrorPath } = params . entrypoint === "tsc"
757
+
758
+ const rawErrorArtifactPath = path . join ( params . resultDirName , rawErrorFileName ) ;
759
+ const replayScriptArtifactPath = path . join ( params . resultDirName , replayScriptFileName ) ;
760
+
761
+ const { status, summary, tsServerResult, replayScriptPath, rawErrorPath } = params . entrypoint === "tsc"
676
762
? await getTscRepoResult ( repo , userTestsDir , oldTsEntrypointPath , newTsEntrypointPath , params . buildWithNewWhenOldFails , downloadDir , diagnosticOutput )
677
- : await getTsServerRepoResult ( repo , userTestsDir , oldTsEntrypointPath , newTsEntrypointPath , downloadDir , path . join ( params . resultDirName , replayScriptFileName ) , path . join ( params . resultDirName , rawErrorFileName ) , diagnosticOutput , isPr ) ;
763
+ : await getTsServerRepoResult ( repo , userTestsDir , oldTsEntrypointPath , newTsEntrypointPath , downloadDir , replayScriptArtifactPath , rawErrorArtifactPath , diagnosticOutput , isPr ) ;
678
764
console . log ( `Repo ${ repo . url ?? repo . name } had status "${ status } "` ) ;
679
765
statusCounts [ status ] = ( statusCounts [ status ] ?? 0 ) + 1 ;
680
- if ( summary ) {
681
766
767
+ if ( summary ) {
682
768
const resultFileName = `${ repoPrefix } .${ resultFileNameSuffix } ` ;
683
769
await fs . promises . writeFile ( path . join ( resultDirPath , resultFileName ) , summary , { encoding : "utf-8" } ) ;
770
+ }
771
+
772
+ if ( tsServerResult ) {
773
+ const replayScriptPath = path . join ( downloadDir , path . basename ( replayScriptArtifactPath ) ) ;
774
+ const repoDir = path . join ( downloadDir , repo . name ) ;
775
+
776
+ let commit : string | undefined ;
777
+ try {
778
+ console . log ( "Extracting commit SHA for repro steps" ) ;
779
+ commit = ( await execAsync ( repoDir , `git rev-parse @` ) ) . trim ( )
780
+ }
781
+ catch {
782
+ //noop
783
+ }
684
784
785
+ summaries . push ( {
786
+ tsServerResult,
787
+ repo,
788
+ oldTsEntrypointPath,
789
+ rawErrorArtifactPath,
790
+ replayScript : fs . readFileSync ( replayScriptPath , { encoding : "utf-8" } ) . split ( / \r ? \n / ) . slice ( - 5 ) . join ( "\n" ) ,
791
+ downloadDir,
792
+ replayScriptArtifactPath,
793
+ replayScriptName : path . basename ( replayScriptArtifactPath ) ,
794
+ commit
795
+ } ) ;
796
+ }
797
+
798
+ if ( summary || tsServerResult ) {
685
799
// In practice, there will only be a replay script when the entrypoint is tsserver
686
800
// There can be replay steps without a summary, but then they're not interesting
687
801
if ( replayScriptPath ) {
@@ -690,9 +804,7 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
690
804
if ( rawErrorPath ) {
691
805
await fs . promises . copyFile ( rawErrorPath , path . join ( resultDirPath , rawErrorFileName ) ) ;
692
806
}
693
-
694
807
}
695
-
696
808
}
697
809
finally {
698
810
// Throw away the repo so we don't run out of space
@@ -727,6 +839,25 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
727
839
}
728
840
}
729
841
842
+ // Group errors and create summary files.
843
+ if ( summaries . length > 0 ) {
844
+ const { groupedOldErrors, groupedNewErrors } = groupErrors ( summaries ) ;
845
+
846
+ for ( let [ key , value ] of groupedOldErrors ) {
847
+ const summary = createOldErrorSummary ( value ) ;
848
+ const resultFileName = `!${ key } .${ resultFileNameSuffix } ` ; // Exclamation point makes the file to be put first when ordering.
849
+
850
+ await fs . promises . writeFile ( path . join ( resultDirPath , resultFileName ) , summary , { encoding : "utf-8" } ) ;
851
+ }
852
+
853
+ for ( let [ key , value ] of groupedNewErrors ) {
854
+ const summary = await createNewErrorSummaryAsync ( value ) ;
855
+ const resultFileName = `${ key } .${ resultFileNameSuffix } ` ;
856
+
857
+ await fs . promises . writeFile ( path . join ( resultDirPath , resultFileName ) , summary , { encoding : "utf-8" } ) ;
858
+ }
859
+ }
860
+
730
861
if ( params . tmpfs ) {
731
862
await execAsync ( processCwd , "sudo rm -rf " + downloadDir ) ;
732
863
await execAsync ( processCwd , "sudo rm -rf " + oldTscDirPath ) ;
0 commit comments