@@ -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
+ replayScriptPath : string ;
103
+ repoDir : string ;
104
+ downloadDir : string ;
105
+ replayScriptArtifactPath : string ;
106
+ replayScriptName : 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,173 @@ 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
+ } ;
334
+
335
+ if ( oldServerFailed && ! newServerFailed ) {
336
+ return { status : "Detected interesting changes" , tsServerResult }
337
+ }
338
+ if ( ! newServerFailed ) {
339
+ return { status : "Detected no interesting changes" } ;
340
+ }
307
341
308
- let summary = `## [${ owner } ${ mdEscape ( repo . name ) } ]${ url } \n` ;
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
+ }
309
353
310
- if ( oldServerFailed ) {
311
- const oldServerError = oldSpawnResult ?. stdout
312
- ? prettyPrintServerHarnessOutput ( oldSpawnResult . stdout , /*filter*/ true )
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
+ ${ fs . readFileSync ( summary . replayScriptPath , { encoding : "utf-8" } ) . split ( / \r ? \n / ) . slice ( - 5 ) . join ( "\n" ) }
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
462
try {
370
463
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` ;
464
+ const commit = ( await execAsync ( summary . repoDir , `git rev-parse @` ) ) . trim ( ) ;
465
+ text += `<li>In dir <code>${ summary . repo . name } </code>, run <code>git reset --hard ${ commit } </code></li>\n` ;
373
466
}
374
467
catch {
375
468
}
376
469
}
377
470
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" ;
471
+ if ( summary . tsServerResult . installCommands . length > 1 ) {
472
+ text += "<li><details><summary>Install packages (exact steps are below, but it might be easier to follow the repo readme)</summary><ol>\n" ;
380
473
}
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` ;
474
+ for ( const command of summary . tsServerResult . installCommands ) {
475
+ text += ` <li>In dir <code>${ path . relative ( summary . downloadDir , command . directory ) } </code>, run <code>${ command . tool } ${ command . arguments . join ( " " ) } </code></li>\n` ;
383
476
}
384
- if ( installCommands . length > 1 ) {
385
- summary += "</ol></details>\n" ;
477
+ if ( summary . tsServerResult . installCommands . length > 1 ) {
478
+ text += "</ol></details>\n" ;
386
479
}
387
480
388
481
// 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` ;
482
+ text += `<li>Back in the initial folder, download <code>${ summary . replayScriptArtifactPath } </code> from the <a href="${ artifactFolderUrlPlaceholder } ">artifact folder</a></li>\n` ;
483
+ text += `<li><code>npm install --no-save @typescript/server-replay</code></li>\n` ;
484
+ text += `<li><code>npx tsreplay ./${ summary . repo . name } ./${ summary . replayScriptName } path/to/tsserver.js</code></li>\n` ;
485
+ text += `<li><code>npx tsreplay --help</code> to learn about helpful switches for debugging, logging, etc</li>\n` ;
393
486
394
- summary += `</ol>
487
+ text += `</ol>
395
488
</details>
396
-
397
489
` ;
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
490
}
491
+
492
+ return text ;
409
493
}
410
494
411
495
// Exported for testing
@@ -658,6 +742,8 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
658
742
659
743
const isPr = params . testType === "user" && ! ! params . prNumber
660
744
745
+ var summaries : Summary [ ] = [ ] ;
746
+
661
747
let i = 1 ;
662
748
for ( const repo of repos ) {
663
749
console . log ( `Starting #${ i ++ } / ${ repos . length } : ${ repo . url ?? repo . name } ` ) ;
@@ -672,16 +758,36 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
672
758
: repo . name ;
673
759
const replayScriptFileName = `${ repoPrefix } .${ replayScriptFileNameSuffix } ` ;
674
760
const rawErrorFileName = `${ repoPrefix } .${ rawErrorFileNameSuffix } ` ;
675
- const { status, summary, replayScriptPath, rawErrorPath } = params . entrypoint === "tsc"
761
+
762
+ const rawErrorArtifactPath = path . join ( params . resultDirName , rawErrorFileName ) ;
763
+ const replayScriptArtifactPath = path . join ( params . resultDirName , replayScriptFileName ) ;
764
+
765
+ const { status, summary, tsServerResult, replayScriptPath, rawErrorPath } = params . entrypoint === "tsc"
676
766
? 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 ) ;
767
+ : await getTsServerRepoResult ( repo , userTestsDir , oldTsEntrypointPath , newTsEntrypointPath , downloadDir , replayScriptArtifactPath , rawErrorArtifactPath , diagnosticOutput , isPr ) ;
678
768
console . log ( `Repo ${ repo . url ?? repo . name } had status "${ status } "` ) ;
679
769
statusCounts [ status ] = ( statusCounts [ status ] ?? 0 ) + 1 ;
680
- if ( summary ) {
681
770
771
+ if ( summary ) {
682
772
const resultFileName = `${ repoPrefix } .${ resultFileNameSuffix } ` ;
683
773
await fs . promises . writeFile ( path . join ( resultDirPath , resultFileName ) , summary , { encoding : "utf-8" } ) ;
774
+ }
775
+
776
+ if ( tsServerResult ) {
777
+ summaries . push ( {
778
+ tsServerResult,
779
+ repo,
780
+ oldTsEntrypointPath,
781
+ rawErrorArtifactPath,
782
+ replayScriptPath : path . join ( downloadDir , path . basename ( replayScriptArtifactPath ) ) ,
783
+ repoDir : path . join ( downloadDir , repo . name ) ,
784
+ downloadDir,
785
+ replayScriptArtifactPath,
786
+ replayScriptName : path . basename ( replayScriptArtifactPath ) ,
787
+ } ) ;
788
+ }
684
789
790
+ if ( summary || tsServerResult ) {
685
791
// In practice, there will only be a replay script when the entrypoint is tsserver
686
792
// There can be replay steps without a summary, but then they're not interesting
687
793
if ( replayScriptPath ) {
@@ -690,9 +796,7 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
690
796
if ( rawErrorPath ) {
691
797
await fs . promises . copyFile ( rawErrorPath , path . join ( resultDirPath , rawErrorFileName ) ) ;
692
798
}
693
-
694
799
}
695
-
696
800
}
697
801
finally {
698
802
// Throw away the repo so we don't run out of space
@@ -727,6 +831,25 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
727
831
}
728
832
}
729
833
834
+ // Group errors and create summary files.
835
+ if ( summaries . length > 0 ) {
836
+ const { groupedOldErrors, groupedNewErrors } = groupErrors ( summaries ) ;
837
+
838
+ for ( let [ key , value ] of groupedOldErrors ) {
839
+ const summary = createOldErrorSummary ( value ) ;
840
+ const resultFileName = `!${ key } .${ resultFileNameSuffix } ` ; // Exclamation point makes the file to be put first when ordering.
841
+
842
+ await fs . promises . writeFile ( path . join ( resultDirPath , resultFileName ) , summary , { encoding : "utf-8" } ) ;
843
+ }
844
+
845
+ for ( let [ key , value ] of groupedNewErrors ) {
846
+ const summary = await createNewErrorSummaryAsync ( value ) ;
847
+ const resultFileName = `${ key } .${ resultFileNameSuffix } ` ;
848
+
849
+ await fs . promises . writeFile ( path . join ( resultDirPath , resultFileName ) , summary , { encoding : "utf-8" } ) ;
850
+ }
851
+ }
852
+
730
853
if ( params . tmpfs ) {
731
854
await execAsync ( processCwd , "sudo rm -rf " + downloadDir ) ;
732
855
await execAsync ( processCwd , "sudo rm -rf " + oldTscDirPath ) ;
@@ -993,4 +1116,4 @@ async function downloadTsNpmAsync(cwd: string, version: string, entrypoint: TsEn
993
1116
}
994
1117
995
1118
return { tsEntrypointPath, resolvedVersion } ;
996
- }
1119
+ }
0 commit comments