import * as core from '@actions/core' ; import * as github from '@actions/github' ; const pkg = require ( '../package.json' ) ; import { plural } from './render' ; const USER_AGENT = `${ pkg . name }/${ pkg . version } (${ pkg . bugs . url })` ; type ChecksCreateParamsOutputAnnotations = any ; interface CargoMessage { reason : string ; message : { code : string ; level : string ; message : string ; rendered : string ; spans : DiagnosticSpan [ ] ; } ; } interface DiagnosticSpan { file_name : string ; is_primary : boolean ; line_start : number ; line_end : number ; column_start : number ; column_end : number ; } interface CheckOptions { token : string ; owner : string ; repo : string ; name : string ; head_sha : string ; started_at : string ; context : { rustc : string ; cargo : string ; clippy : string ; } ; } interface Stats { ice : number ; error : number ; warning : number ; note : number ; help : number ; } export class CheckRunner { private annotations : Array < ChecksCreateParamsOutputAnnotations > ; private stats : Stats ; constructor ( ) { this . annotations = [ ] ; this . stats = { ice : 0 , error : 0 , warning : 0 , note : 0 , help : 0 , } ; } public tryPush ( line ) { let contents ; try { contents = JSON . parse ( line ) ; } catch ( error ) { core . debug ( 'Not a JSON, ignoring it' ) ; return ; } if ( contents . reason != 'compiler-message' ) { core . debug ( `Unexpected reason field, ignoring it: ${ contents . reason }` ) ; return ; } if ( contents . message . code === null ) { core . debug ( 'Message code is missing, ignoring it' ) ; return ; } switch ( contents . message . level ) { case 'help' : this . stats . help += 1 ; break ; case 'note' : this . stats . note += 1 ; break ; case 'warning' : this . stats . warning += 1 ; break ; case 'error' : this . stats . error += 1 ; break ; case 'error: internal compiler error' : this . stats . ice += 1 ; break ; default : break ; } this . annotations . push ( CheckRunner . makeAnnotation ( contents ) ) ; } public async executeCheck ( options ) { core . info ( `Clippy results: \
${ this . stats . ice } ICE, ${ this . stats . error } errors, \
${ this . stats . warning } warnings, ${ this . stats . note } notes, \
${ this . stats . help } help` ) ; const client = github . getOctokit ( options . token , { userAgent : USER_AGENT , } ) ; let checkRunId ; try { checkRunId = await this . createCheck ( client , options ) ; } catch ( error ) { if ( process . env . GITHUB_HEAD_REF ) { core . error ( `Unable to create clippy annotations! Reason: ${ error }` ) ; core . warning ( "It seems that this Action is executed from the forked repository." ) ; core . warning ( `GitHub Actions are not allowed to create Check annotations, \
when executed for a forked repos. \
See https://github.com/actions-rs/clippy-check/issues/2 for details.` ) ; core . info ( 'Posting clippy checks here instead.' ) ; this . dumpToStdout ( ) ; if ( this . getConclusion ( ) == 'failure' ) { throw new Error ( 'Exiting due to clippy errors' ) ; } else { return ; } } else { throw error ; } } try { if ( this . isSuccessCheck ( ) ) { await this . successCheck ( client , checkRunId , options ) ; } else { await this . runUpdateCheck ( client , checkRunId , options ) ; } } catch ( error ) { await this . cancelCheck ( client , checkRunId , options ) ; throw error ; } } private async createCheck ( client , options ) { const response = await client . checks . create ( { owner : options . owner , repo : options . repo , name : options . name , head_sha : options . head_sha , status : 'in_progress' , } ) ; return response . data . id ; } private async runUpdateCheck ( client , checkRunId , options ) { let annotations = this . getBucket ( ) ; while ( annotations . length > 0 ) { let req = { owner : options . owner , repo : options . repo , name : options . name , check_run_id : checkRunId , output : { title : options . name , summary : this . getSummary ( ) , text : this . getText ( options . context ) , annotations : annotations , } } ; if ( this . annotations . length > 0 ) { core . debug ( 'This is not the last iteration, marking check as "in_progress"' ) ; req . status = 'in_progress' ; } else { const conclusion = this . getConclusion ( ) ; core . debug ( `This is a last iteration, marking check as "completed", conclusion: ${ conclusion }` ) ; req . status = 'completed' ; req . conclusion = conclusion ; req . completed_at = new Date ( ) . toISOString ( ) ; } await client . checks . update ( req ) ; annotations = this . getBucket ( ) ; } return ; } private async successCheck ( client , checkRunId , options ) { let req = { owner : options . owner , repo : options . repo , name : options . name , check_run_id : checkRunId , status : 'completed' , conclusion : this . getConclusion ( ) , completed_at : new Date ( ) . toISOString ( ) , output : { title : options . name , summary : this . getSummary ( ) , text : this . getText ( options . context ) , } } ; await client . checks . update ( req ) ; return ; } private async cancelCheck ( client , checkRunId , options ) { let req = { owner : options . owner , repo : options . repo , name : options . name , check_run_id : checkRunId , status : 'completed' , conclusion : 'cancelled' , completed_at : new Date ( ) . toISOString ( ) , output : { title : options . name , summary : 'Unhandled error' , text : 'Check was cancelled due to unhandled error. Check the Action logs for details.' , } } ; await client . checks . update ( req ) ; return ; } private dumpToStdout ( ) { for ( const annotation of this . annotations ) { core . info ( annotation . message ) ; } } private getBucket ( ) { let annotations = [ ] ; while ( annotations . length < 50 ) { const annotation = this . annotations . pop ( ) ; if ( annotation ) { annotations . push ( annotation ) ; } else { break ; } } core . debug ( `Prepared next annotations bucket, ${ annotations . length } size` ) ; return annotations ; } private getSummary ( ) { let blocks = [ ] ; if ( this . stats . ice > 0 ) { blocks . push ( `${ this . stats . ice } internal compiler error${ plural ( this . stats . ice ) }` ) ; } if ( this . stats . error > 0 ) { blocks . push ( `${ this . stats . error } error${ plural ( this . stats . error ) }` ) ; } if ( this . stats . warning > 0 ) { blocks . push ( `${ this . stats . warning } warning${ plural ( this . stats . warning ) }` ) ; } if ( this . stats . note > 0 ) { blocks . push ( `${ this . stats . note } note${ plural ( this . stats . note ) }` ) ; } if ( this . stats . help > 0 ) { blocks . push ( `${ this . stats . help } help message${ plural ( this . stats . help ) }` ) ; } return blocks . join ( ', ' ) ; } private getText ( context ) { return `## Results

| Message level           | Amount                |
| ----------------------- | --------------------- |
| Internal compiler error | ${ this . stats . ice }     |
| Error                   | ${ this . stats . error }   |
| Warning                 | ${ this . stats . warning } |
| Note                    | ${ this . stats . note }    |
| Help                    | ${ this . stats . help }    |

## Versions

* ${ context . rustc }
* ${ context . cargo }
* ${ context . clippy }
` ; } private getConclusion ( ) { if ( this . stats . ice > 0 || this . stats . error > 0 ) { return 'failure' ; } else { return 'success' ; } } private isSuccessCheck ( ) { return this . stats . ice == 0 && this . stats . error == 0 && this . stats . warning == 0 && this . stats . note == 0 && this . stats . help == 0 ; } static makeAnnotation ( contents ) { const primarySpan = contents . message . spans . find ( ( span ) => span . is_primary == true ) ; if ( null == primarySpan ) { throw new Error ( 'Unable to find primary span for message' ) ; } let annotation_level ; switch ( contents . message . level ) { case 'help' : case 'note' : annotation_level = 'notice' ; break ; case 'warning' : annotation_level = 'warning' ; break ; default : annotation_level = 'failure' ; break ; } let annotation = { path : primarySpan . file_name , start_line : primarySpan . line_start , end_line : primarySpan . line_end , annotation_level : annotation_level , title : contents . message . message , message : contents . message . rendered , } ; if ( primarySpan . line_start == primarySpan . line_end ) { annotation . start_column = primarySpan . column_start ; annotation . end_column = primarySpan . column_end ; } return annotation ; } }