diff --git a/ap-file-types.php b/ap-file-types.php new file mode 100644 index 000000000..286cc2b51 --- /dev/null +++ b/ap-file-types.php @@ -0,0 +1,107 @@ + $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'commit_id' => $options['commit'], + 'autoapprove' => $options['autoapprove'], + + 'autoapprove-filetypes' => + $options['autoapprove-filetypes'], + ) + ); + + + $prs_implicated = vipgoci_github_prs_implicated( + $options['repo-owner'], + $options['repo-name'], + $options['commit'], + $options['token'], + $options['branches-ignore'] + ); + + + foreach ( $prs_implicated as $pr_item ) { + $pr_diff = vipgoci_github_diffs_fetch( + $options['repo-owner'], + $options['repo-name'], + $options['token'], + $pr_item->base->sha, + $options['commit'], + true + ); + + + foreach ( $pr_diff as + $pr_diff_file_name => $pr_diff_contents + ) { + /* + * If the file is already in the array + * of approved files, do not do anything. + */ + if ( isset( + $auto_approved_files_arr[ + $pr_diff_file_name + ] + ) ) { + continue; + } + + $pr_diff_file_extension = vipgoci_file_extension( + $pr_diff_file_name + ); + + /* + * Check if the extension of the file + * is in a list of auto-approvable + * file extensions. + */ + if ( in_array( + $pr_diff_file_extension, + $options['autoapprove-filetypes'], + true + ) ) { + $auto_approved_files_arr[ + $pr_diff_file_name + ] = 'autoapprove-filetypes'; + } + } + } + + /* + * Reduce memory-usage as possible + */ + unset( $prs_implicated ); + unset( $pr_diff ); + unset( $pr_item ); + unset( $pr_diff_file_extension ); + unset( $pr_diff_file_name ); + + gc_collect_cycles(); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'ap_file_types' ); +} + diff --git a/ap-hashes-api.php b/ap-hashes-api.php new file mode 100644 index 000000000..3dcd136aa --- /dev/null +++ b/ap-hashes-api.php @@ -0,0 +1,385 @@ + $file_path, + + 'file_extension' + => $file_info_extension, + + 'file_extensions_approvable' + => $file_extensions_approvable, + ) + ); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'hashes_api_scan_file' ); + + return null; + } + + + vipgoci_log( + 'Checking if file is already approved in ' . + 'hashes-to-hashes API', + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'commit' => $options['commit'], + 'file_path' => $file_path, + ) + ); + + /* + * Try to read file from disk, then + * get rid of whitespaces in the file + * and calculate SHA1 hash from the whole. + */ + + $file_contents = vipgoci_gitrepo_fetch_committed_file( + $options['repo-owner'], + $options['repo-name'], + $options['token'], + $options['commit'], + $file_path, + $options['local-git-repo'] + ); + + if ( false === $file_contents ) { + vipgoci_log( + 'Unable to read file', + array( + 'file_path' => $file_path, + ) + ); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'hashes_api_scan_file' ); + + return null; + } + + vipgoci_log( + 'Saving file from git-repository into temporary file ' . + 'in order to strip any whitespacing from it', + array( + 'file_path' => $file_path, + ), + 2 + ); + + + $file_temp_path = vipgoci_save_temp_file( + $file_path, + null, + $file_contents + ); + + $file_contents_stripped = php_strip_whitespace( + $file_temp_path + ); + + + $file_sha1 = sha1( $file_contents_stripped ); + + unlink( $file_temp_path ); + unset( $file_contents ); + unset( $file_contents_stripped ); + + + /* + * Ask the API for information about + * the specific hash we calculated. + */ + + vipgoci_log( + 'Asking hashes-to-hashes HTTP API if hash of file is ' . + 'known', + array( + 'file_path' => $file_path, + 'file_sha1' => $file_sha1, + ), + 2 + ); + + $hashes_to_hashes_url = + $options['hashes-api-url'] . + '/v1/hashes/id/' . + rawurlencode( $file_sha1 ); + + /* + * Not really asking GitHub here, + * but we can re-use the function + * for this purpose. + */ + + $file_hashes_info = + vipgoci_github_fetch_url( + $hashes_to_hashes_url, + array( + 'oauth_consumer_key' => + $options['hashes-oauth-consumer-key'], + + 'oauth_consumer_secret' => + $options['hashes-oauth-consumer-secret'], + + 'oauth_token' => + $options['hashes-oauth-token'], + + 'oauth_token_secret' => + $options['hashes-oauth-token-secret'], + ) + ); + + + /* + * Try to parse, and check for errors. + */ + + if ( false !== $file_hashes_info ) { + $file_hashes_info = json_decode( + $file_hashes_info, + true + ); + } + + + if ( + ( false === $file_hashes_info ) || + ( null === $file_hashes_info ) || + ( isset( $file_hashes_info['data']['status'] ) ) + ) { + vipgoci_log( + 'Unable to get information from ' . + 'hashes-to-hashes HTTP API', + array( + 'hashes_to_hashes_url' => $hashes_to_hashes_url, + 'file_path' => $file_path, + 'http_reply' => $file_hashes_info, + ) + ); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'hashes_api_scan_file' ); + + return null; + } + + $file_approved = null; + + /* + * Only approve file if all info-items show + * the file to be approved. + */ + + foreach( $file_hashes_info as $file_hash_info ) { + if ( ! isset( $file_hash_info[ 'status' ] ) ) { + $file_approved = false; + } + + if ( + ( 'false' === $file_hash_info[ 'status' ] ) || + ( false === $file_hash_info[ 'status' ] ) + ) { + $file_approved = false; + } + + else if ( + ( 'true' === $file_hash_info[ 'status' ] ) || + ( true === $file_hash_info[ 'status' ] ) + ) { + /* + * Only update approval-flag if we have not + * seen any other approvals, and if we have + * not seen any rejections. + */ + if ( null === $file_approved ) { + $file_approved = true; + } + } + } + + + /* + * If no approval is seen, assume it is not + * approved at all. + */ + + if ( null === $file_approved ) { + $file_approved = false; + } + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'hashes_api_scan_file' ); + + return $file_approved; +} + + +/* + * Scan a particular commit, look for altered + * files in the Pull-Request we are associated with + * and for each of these files, check if they + * are approved in the hashes-to-hashes API. + */ +function vipgoci_ap_hashes_api_scan_commit( + $options, + &$commit_issues_submit, + &$commit_issues_stats, + &$auto_approved_files_arr +) { + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'hashes_api_scan' ); + + vipgoci_log( + 'Scanning altered or new files affected by Pull-Request(s) ' . + 'using hashes-to-hashes API', + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'commit_id' => $options['commit'], + 'hashes-api' => $options['hashes-api'], + 'hashes-api-url' => $options['hashes-api-url'], + ) + ); + + + $prs_implicated = vipgoci_github_prs_implicated( + $options['repo-owner'], + $options['repo-name'], + $options['commit'], + $options['token'], + $options['branches-ignore'] + ); + + + foreach ( $prs_implicated as $pr_item ) { + $pr_diff = vipgoci_github_diffs_fetch( + $options['repo-owner'], + $options['repo-name'], + $options['token'], + $pr_item->base->sha, + $options['commit'], + true + ); + + + foreach( $pr_diff as + $pr_diff_file_name => $pr_diff_contents + ) { + /* + * If it is already approved, + * do not do anything. + */ + + if ( isset( + $auto_approved_files_arr[ + $pr_diff_file_name + ] + ) ) { + continue; + } + + /* + * Check if the hashes-to-hashes database + * recognises this file, and check its + * status. + */ + + $approval_status = vipgoci_ap_hashes_api_file_approved( + $options, + $pr_diff_file_name + ); + + + /* + * Add the file to a list of approved files + * of these affected by the Pull-Request. + */ + if ( true === $approval_status ) { + vipgoci_log( + 'File is approved in ' . + 'hashes-to-hashes API', + array( + 'file_name' => $pr_diff_file_name, + ) + ); + + $auto_approved_files_arr[ + $pr_diff_file_name + ] = 'autoapprove-hashes-to-hashes'; + } + + else if ( false === $approval_status ) { + vipgoci_log( + 'File is not approved in ' . + 'hashes-to-hashes API', + array( + 'file_name' => $pr_diff_file_name, + ) + ); + } + + else if ( null === $approval_status ) { + vipgoci_log( + 'Could not determine if file is approved ' . + 'in hashes-to-hashes API', + array( + 'file_name' => $pr_diff_file_name, + ) + ); + } + } + } + + + /* + * Reduce memory-usage as possible + */ + + unset( $prs_implicated ); + unset( $pr_item ); + unset( $pr_diff ); + unset( $pr_diff_contents ); + unset( $approval_status ); + + gc_collect_cycles(); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'hashes_api_scan' ); +} + diff --git a/ap-svg-files.php b/ap-svg-files.php new file mode 100644 index 000000000..c92c4369e --- /dev/null +++ b/ap-svg-files.php @@ -0,0 +1,185 @@ + $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'commit_id' => $options['commit'], + ) + ); + + + $prs_implicated = vipgoci_github_prs_implicated( + $options['repo-owner'], + $options['repo-name'], + $options['commit'], + $options['token'], + $options['branches-ignore'] + ); + + + foreach ( $prs_implicated as $pr_item ) { + $pr_diff = vipgoci_github_diffs_fetch( + $options['repo-owner'], + $options['repo-name'], + $options['token'], + $pr_item->base->sha, + $options['commit'], + true + ); + + + foreach ( $pr_diff as + $pr_diff_file_name => $pr_diff_contents + ) { + $pr_diff_file_extension = vipgoci_file_extension( + $pr_diff_file_name + ); + + /* + * If not a SVG file, do not do anything. + */ + + if ( + 'svg' !== + $pr_diff_file_extension + ) { + continue; + } + + /* + * If the file is already in the array + * of approved files, do not do anything. + */ + if ( isset( + $auto_approved_files_arr[ + $pr_diff_file_name + ] + ) ) { + continue; + } + + /* + * Scan the SVG file, get the results. + */ + + $tmp_scan_results = vipgoci_svg_scan_single_file( + $options, + $pr_diff_file_name + ); + + $file_issues_arr_master = + $tmp_scan_results['file_issues_arr_master']; + + /* + * Check for failure + */ + if ( + ( ! isset( + $file_issues_arr_master['totals'] + ) ) + || + ( ! isset( + $file_issues_arr_master['totals']['errors'] + ) ) + || + ( ! isset( + $file_issues_arr_master['totals']['warnings'] + ) ) + ) { + vipgoci_log( + 'Not adding SVG file to list of ' . + 'approved files as a failure occurred', + + array( + 'file_name' => + $pr_diff_file_name, + 'file_issues_arr_master' => + $file_issues_arr_master, + ) + ); + } + + + /* + * If no issues were found, we + * can approve this file. + */ + else if ( + ( 0 === + $file_issues_arr_master['totals']['errors'] + ) + && + ( 0 === + $file_issues_arr_master['totals']['warnings'] + ) + ) { + vipgoci_log( + 'Adding SVG file to list of approved ' . + 'files, as no PHPCS-issues ' . + 'were found', + array( + 'file_name' => + $pr_diff_file_name, + ) + ); + + $auto_approved_files_arr[ + $pr_diff_file_name + ] = 'ap-svg-files'; + } + + else { + vipgoci_log( + 'Not adding SVG file to list of ' . + 'approved files as issues ' . + 'were found', + array( + 'file_name' => + $pr_diff_file_name, + 'file_issues_arr_master' => + $file_issues_arr_master, + ) + ); + } + } + } + + /* + * Reduce memory-usage as possible + */ + unset( $tmp_scan_results ); + unset( $prs_implicated ); + unset( $pr_diff ); + unset( $pr_item ); + unset( $pr_diff_file_extension ); + unset( $pr_diff_file_name ); + unset( $file_issues_arr_master ); + + gc_collect_cycles(); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'ap_svg_files' ); +} + diff --git a/auto-approval.php b/auto-approval.php index f48a01484..e4fde8014 100644 --- a/auto-approval.php +++ b/auto-approval.php @@ -1,5 +1,425 @@ number . ' ' . + 'as it contains ' . + 'files which are not ' . + 'automatically approvable' . + ' -- PR URL: https://github.com/' . + rawurlencode( $options['repo-owner'] ) . + '/' . + rawurlencode( $options['repo-name'] ) . + '/pull/' . + (int) $pr_item->number . ' ', + + array( + 'repo_owner' => + $options['repo-owner'], + + 'repo_name' => + $options['repo-name'], + + 'pr_number' => + $pr_item->number, + + 'autoapprove-filetypes' => + $options['autoapprove-filetypes'], + + 'auto_approved_files_arr' => + $auto_approved_files_arr, + + 'files_seen' => $files_seen, + ), + 0, + true // Send to IRC + ); + + /* + * Temporary: Just while we test the + * approval-logic. + */ + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'vipgoci_auto_approval_non_approval' ); + + return; + + + if ( false === $pr_label ) { + vipgoci_log( + 'Will not attempt to remove label ' . + 'from issue as it does not ' . + 'exist', + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'pr_number' => $pr_item->number, + 'label_name' => $options['autoapprove-label'], + ) + ); + } + + else { + /* + * Remove auto-approve label + */ + vipgoci_github_label_remove_from_pr( + $options['repo-owner'], + $options['repo-name'], + $options['token'], + (int) $pr_item->number, + $pr_label->name, + $options['dry-run'] + ); + } + + /* + * Loop through approved PHP and JS files, + * adding comment for each about it + * being approved in the hashes-to-hashes API. + */ + foreach( + $auto_approved_files_arr as + $approved_file => + $approved_file_system + ) { + + if ( + $approved_file_system !== + 'autoapprove-hashes-to-hashes' + ) { + /* + * If not autoapproved by hashes-to-hashes, + * do not comment on it. Only PHP and JS files + * are auto-approved by hashes-to-hashes. + */ + continue; + } + + $results[ + 'issues' + ][ + (int) $pr_item->number + ] + [] = array( + 'type' => VIPGOCI_STATS_HASHES_API, + 'file_name' => $approved_file, + 'file_line' => 1, + 'issue' => array( + 'message'=> VIPGOCI_FILE_IS_APPROVED_MSG, + + 'source' + => 'WordPressVIPMinimum.' . + 'Info.ApprovedHashesToHashesAPI', + + 'severity' => 1, + 'fixable' => false, + 'type' => 'INFO', + 'line' => 1, + 'column' => 1, + 'level' =>'INFO' + ) + ); + + $results[ + 'stats' + ][ + VIPGOCI_STATS_HASHES_API + ][ + (int) $pr_item->number + ][ + 'info' + ]++; + } + + + /* + * Remove any 'file is approved in ...' comments, + * but only for files that are no longer approved. + */ + vipgoci_log( + 'Removing any comments indicating a file is approved ' . + 'for files that are not approved anymore', + array( + 'pr_number' => $pr_item->number, + ) + ); + + $pr_comments = vipgoci_github_pr_reviews_comments_get_by_pr( + $options, + $pr_item->number, + array( + 'login' => 'myself', + ) + ); + + foreach( $pr_comments as $pr_comment_item ) { + /* + * Skip approved files. + */ + if ( isset( + $auto_approved_files_arr[ + $pr_comment_item->path + ] + ) ) { + continue; + } + + /* + * If we find the 'approved in hashes-to-hashes ...' + * message, we can safely remove the comment. + */ + if ( false !== strpos( + $pr_comment_item->body, + VIPGOCI_FILE_IS_APPROVED_MSG + ) ) { + vipgoci_github_pr_reviews_comments_delete( + $options, + $pr_comment_item->id + ); + } + } + + + /* + * Get any approving reviews for the Pull-Request + * submitted by us. Then dismiss them. + */ + + vipgoci_log( + 'Dismissing any approving reviews for ' . + 'the Pull-Request, as it is not ' . + 'approved anymore', + array( + 'pr_number' => $pr_item->number, + ) + ); + + $pr_item_reviews = vipgoci_github_pr_reviews_get( + $options['repo-owner'], + $options['repo-name'], + (int) $pr_item->number, + $options['token'], + array( + 'login' => 'myself', + 'state' => array( 'APPROVED' ), + ) + ); + + /* + * Dismiss any approving reviews. + */ + + foreach( $pr_item_reviews as $pr_item_review ) { + vipgoci_github_pr_review_dismiss( + $options['repo-owner'], + $options['repo-name'], + (int) $pr_item->number, + (int) $pr_item_review->id, + 'Dismissing obsolete review; not approved any longer', + $options['token'] + ); + } + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'vipgoci_auto_approval_non_approval' ); +} + +/* + * Approve a particular Pull-Request, + * alter label for the PR if needed, + * remove old comments, and log everything + * we do. + */ +function vipgoci_autoapproval_do_approve( + $options, + $pr_item, + $pr_label, + &$auto_approved_files_arr, + $files_seen +) { + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'vipgoci_autoapproval_do_approve' ); + + vipgoci_counter_report( + 'do', + 'github_pr_approval', + 1 + ); + + vipgoci_log( + ( $options['dry-run'] === true + ? 'Would ' : 'Will ' ) . + 'auto-approve Pull-Request #' . + (int) $pr_item->number . ' ' . + 'as it alters or creates ' . + 'only files that can be ' . + 'automatically approved' . + ' -- PR URL: https://github.com/' . + rawurlencode( $options['repo-owner'] ) . + '/' . + rawurlencode( $options['repo-name'] ) . + '/pull/' . + (int) $pr_item->number . ' ', + + array( + 'repo_owner' + => $options['repo-owner'], + + 'repo_name' + => $options['repo-name'], + + 'pr_number' + => (int) $pr_item->number, + + 'commit_id' + => $options['commit'], + + 'dry_run' + => $options['dry-run'], + + 'autoapprove-filetypes' => + $options['autoapprove-filetypes'], + + 'auto_approved_files_arr' => + $auto_approved_files_arr, + + 'files_seen' => $files_seen, + ), + 0, + true // Send to IRC + ); + + /* + * Temporary: Just while we test the + * approval-logic. + */ + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'vipgoci_autoapproval_do_approve' ); + + return; + + + /* + * Actually approve, if not in dry-mode. + * Also add a label to the Pull-Request + * if applicable. + */ + vipgoci_github_approve_pr( + $options['repo-owner'], + $options['repo-name'], + $options['token'], + $pr_item->number, + $options['commit'], + 'Auto-approved Pull-Request #' . + (int) $pr_item->number . ' as it ' . + 'contains only auto-approvable files' . + '-- either pre-approved files or file-types that are ' . + 'auto-approvable (' . + implode( ', ', $options['autoapprove-filetypes'] ) . + ').', + $options['dry-run'] + ); + + + /* + * Add label to Pull-Request, but + * only if it is not associated already. + * If it is already associated, just log + * that fact. + */ + if ( false === $pr_label ) { + vipgoci_github_label_add_to_pr( + $options['repo-owner'], + $options['repo-name'], + $options['token'], + $pr_item->number, + $options['autoapprove-label'], + $options['dry-run'] + ); + } + + else { + vipgoci_log( + 'Will not add label to issue, ' . + 'as it already exists', + array( + 'repo_owner' => + $options['repo-owner'], + 'repo_name' => + $options['repo-name'], + 'pr_number' => + $pr_item->number, + 'label_name' => + $options['autoapprove-label'], + ) + ); + } + + /* + * Remove any comments indicating that a file is + * approved -- we want to get rid of these, as they + * are useless to reviewers at this point. The PR is + * approved anyway. + */ + vipgoci_log( + 'Removing any previously submitted comments ' . + 'indicating that a particular file ' . + 'is approved as the whole ' . + 'Pull-Request is approved', + array( + 'pr_number' => $pr_item->number, + ) + ); + + $pr_comments = vipgoci_github_pr_reviews_comments_get_by_pr( + $options, + $pr_item->number, + array( + 'login' => 'myself', + ) + ); + + foreach( $pr_comments as $pr_comment_item ) { + /* + * If we find the 'approved in hashes-to-hashes ...' + * message, we can safely remove the comment. + */ + if ( false !== strpos( + $pr_comment_item->body, + VIPGOCI_FILE_IS_APPROVED_MSG + ) ) { + vipgoci_github_pr_reviews_comments_delete( + $options, + $pr_comment_item->id + ); + } + } + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'vipgoci_autoapproval_do_approve' ); +} + /* * Process auto-approval(s) of the Pull-Request(s) * involved with the commit specified. @@ -10,12 +430,18 @@ * of files, the function will auto-approve them, and else not. * * Note that the --skip-folders argument is ignored - * in this function. + * in this function. This is intentional, because if this + * function did not ignore these folders, it might approve + * Pull-Requests containing files that are actually used + * in production and could contain dangerous code. */ -function vipgoci_auto_approval( $options ) { - - vipgoci_runtime_measure( 'start', 'auto_approve_commit' ); +function vipgoci_auto_approval( + $options, + &$auto_approved_files_arr, + &$results +) { + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'auto_approve_commit' ); vipgoci_log( 'Doing auto-approval', @@ -25,8 +451,8 @@ function vipgoci_auto_approval( $options ) { 'commit_id' => $options['commit'], 'autoapprove' => $options['autoapprove'], - 'autoapprove-filetypes' => - $options['autoapprove-filetypes'], + 'autoapproved_files_arr' => + $auto_approved_files_arr, ) ); @@ -46,7 +472,8 @@ function vipgoci_auto_approval( $options ) { $options['repo-name'], $options['token'], $pr_item->base->sha, - $options['commit'] + $options['commit'], + true ); @@ -55,6 +482,11 @@ function vipgoci_auto_approval( $options ) { $files_seen = array(); + /* + * Loop through all files that are + * altered by the Pull-Request, look for + * files that can be auto-approved. + */ foreach( $pr_diff as $pr_diff_file_name => $pr_diff_contents ) { @@ -63,18 +495,15 @@ function vipgoci_auto_approval( $options ) { $files_seen[] = $pr_diff_file_name; - $pr_diff_file_extension = pathinfo( - $pr_diff_file_name, - PATHINFO_EXTENSION - ); - - - if ( ! in_array( - strtolower( - $pr_diff_file_extension - ), - $options['autoapprove-filetypes'], - true + /* + * Is file in array of files + * that can be auto-approved? + * If not, we cannot auto-approve. + */ + if ( ! isset( + $auto_approved_files_arr[ + $pr_diff_file_name + ] ) ) { $can_auto_approve = false; break; @@ -98,10 +527,24 @@ function vipgoci_auto_approval( $options ) { vipgoci_log( 'No action taken with Pull-Request #' . (int) $pr_item->number . ' ' . - 'since no files were found', + 'since no files were found' . + ' -- PR URL: https://github.com/' . + rawurlencode( $options['repo-owner'] ) . + '/' . + rawurlencode( $options['repo-name'] ) . + '/pull/' . + (int) $pr_item->number . ' ', + array( + 'auto_approved_files_arr' => + $auto_approved_files_arr, + 'files_seen' => $files_seen, - ) + 'pr_number' => (int) $pr_item->number, + 'pr_diff' => $pr_diff, + ), + 0, + true ); } @@ -109,132 +552,27 @@ function vipgoci_auto_approval( $options ) { ( true === $did_foreach ) && ( false === $can_auto_approve ) ) { - vipgoci_log( - 'Will not auto-approve Pull-Request #' . - (int) $pr_item->number . ' ' . - 'as it contains ' . "\n\t" . - 'file-types which are not ' . - 'automatically approvable', - array( - 'autoapprove-filetypes' => - $options['autoapprove-filetypes'], - - 'files_seen' => $files_seen, - ) + vipgoci_auto_approval_non_approval( + $options, + $results, + $pr_item, + $pr_label, + $auto_approved_files_arr, + $files_seen ); - - - if ( false === $pr_label ) { - vipgoci_log( - 'Will not attempt to remove label ' . - 'from issue as it does not ' . - 'exist', - array( - 'repo_owner' => $options['repo-owner'], - 'repo_name' => $options['repo-name'], - 'pr_number' => $pr_item->number, - 'label_name' => $options['autoapprove-label'], - ) - ); - } - - else { - /* - * Remove auto-approve label - */ - vipgoci_github_label_remove_from_pr( - $options['repo-owner'], - $options['repo-name'], - $options['token'], - (int) $pr_item->number, - $pr_label->name, - $options['dry-run'] - ); - } } else if ( ( true === $did_foreach ) && ( true === $can_auto_approve ) ) { - vipgoci_log( - ( $options['dry-run'] === true - ? 'Would ' : 'Will ' ) . - 'auto-approve Pull-Request #' . - (int) $pr_item->number . ' ' . - 'as it alters or creates ' . "\n\t" . - 'only file-types that can be ' . - 'automatically approved', - array( - 'repo_owner' - => $options['repo-owner'], - - 'repo_name' - => $options['repo-name'], - - 'commit_id' - => $options['commit'], - - 'dry_run' - => $options['dry-run'], - - 'autoapprove-filetypes' => - $options['autoapprove-filetypes'], - - 'files_seen' => $files_seen, - ) - ); - - - /* - * Actually approve, if not in dry-mode. - * Also add a label to the Pull-Request - * if applicable. - */ - vipgoci_github_approve_pr( - $options['repo-owner'], - $options['repo-name'], - $options['token'], - $pr_item->number, - $options['commit'], - $options['autoapprove-filetypes'], - $options['dry-run'] + vipgoci_autoapproval_do_approve( + $options, + $pr_item, + $pr_label, + $auto_approved_files_arr, + $files_seen ); - - - /* - * Add label to Pull-Request, but - * only if it is not associated already. - * If it is already associated, just log - * that fact. - */ - if ( false === $pr_label ) { - vipgoci_github_label_add_to_pr( - $options['repo-owner'], - $options['repo-name'], - $options['token'], - $pr_item->number, - $options['autoapprove-label'], - $options['dry-run'] - ); - } - - else { - vipgoci_log( - 'Will not add label to issue, ' . - 'as it already exists', - array( - 'repo_owner' => - $options['repo-owner'], - 'repo_name' => - $options['repo-name'], - 'pr_number' => - $pr_item->number, - 'label_name' => - $options['autoapprove-label'], - ) - ); - } } unset( $files_seen ); @@ -243,11 +581,19 @@ function vipgoci_auto_approval( $options ) { /* * Reduce memory-usage as possible */ - unset( $prs_implicated ); + unset( $pr_diff ); + unset( $pr_diff_file_name ); + unset( $pr_diff_contents ); + unset( $pr_item ); + unset( $pr_label ); + unset( $prs_implicated ); + unset( $files_seen ); + unset( $did_foreach ); + unset( $can_auto_approve ); gc_collect_cycles(); - vipgoci_runtime_measure( 'stop', 'auto_approve_commit' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'auto_approve_commit' ); } diff --git a/defines.php b/defines.php new file mode 100644 index 000000000..de3c9e6ce --- /dev/null +++ b/defines.php @@ -0,0 +1,57 @@ + $value ) { + $parameters_arr_new[ rawurlencode( $key ) ] = + rawurlencode( $value ); + } + + /* + * Also these two should not be part of the + * signature. + */ + unset( $parameters_arr_new['oauth_token_secret'] ); + unset( $parameters_arr_new['oauth_consumer_secret'] ); + + /* + * Sort the parameters alphabetically. + */ + ksort( $parameters_arr_new ); + + + /* + * Loop through the parameters, and add them + * to a temporary 'base string' according to the standard. + */ + + $delimiter = ''; + $base_string_tmp = ''; + + foreach( $parameters_arr_new as $key => $value ) { + $base_string_tmp .= + $delimiter . + $key . + '=' . + $value; + + $delimiter = '&'; + } + + /* + * Then add the temporary 'base string' to the + * permanent 'base string'. + */ + $base_string .= rawurlencode( + $base_string_tmp + ); + + + /* + * Now calculate hash, using the + * 'base string' as input, and + * secrets as key. + */ + $hash_raw = hash_hmac( + 'sha1', + $base_string, + $parameters_arr['oauth_consumer_secret'] . '&' . + $parameters_arr['oauth_token_secret'], + true + ); + + /* + * Return it base64 encoded. + */ + return base64_encode( $hash_raw ); +} + + +/* + * Create and set HTTP header for OAuth 1.0a requests, + * including timestamp, nonce, signature method + * (all part of the header) and then actually sign + * the request. Returns with a full HTTP header for + * a OAuth 1.0a HTTP request. + */ +function vipgoci_oauth1_headers_get( + $http_method, + $github_url, + $github_token +) { + + /* + * Set signature-method header, static. + */ + $github_token['oauth_signature_method'] = + 'HMAC-SHA1'; + + /* + * Set timestamp and nonce. + */ + $github_token['oauth_timestamp'] = (string) ( time() - 1); + + $github_token['oauth_nonce'] = (string) md5( + openssl_random_pseudo_bytes( 100 ) + ); + + /* + * Get the signature for the header. + */ + $github_token['oauth_signature'] = + vipgoci_oauth1_signature_get_hmac_sha1( + $http_method, + $github_url, + $github_token + ); + + /* + * Those are not needed after this point, + * so we remove them to limit any risk + * of information leakage. + */ + unset( $github_token['oauth_token_secret' ] ); + unset( $github_token['oauth_consumer_secret' ] ); + + /* + * Actually create the full HTTP header + */ + + $res_header = 'OAuth '; + $sep = ''; + + foreach( + $github_token as + $github_token_key => + $github_token_value + ) { + if ( strpos( + $github_token_key, + 'oauth_' + ) !== 0 ) { + /* + * If the token_key does not + * start with 'oauth_' we skip to + * avoid information-leakage. + */ + continue; + } + + $res_header .= + $sep . + $github_token_key . '="' . + rawurlencode( $github_token_value ) . + '"'; + $sep = ', '; + } + + /* + * Return the header. + */ + return $res_header; } @@ -267,13 +444,13 @@ function vipgoci_github_post_url( * and keep count of how many requests we do. */ - vipgoci_runtime_measure( 'start', 'github_api' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'github_api_post' ); vipgoci_counter_report( 'do', 'github_api_request_post', 1 ); $resp_data = curl_exec( $ch ); - vipgoci_runtime_measure( 'stop', 'github_api' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'github_api_post' ); $resp_headers = vipgoci_curl_headers( @@ -397,7 +574,6 @@ function vipgoci_github_post_url( return $ret_val; } - /* * Make a GET request to GitHub, for the URL * provided, using the access-token specified. @@ -435,11 +611,38 @@ function vipgoci_github_fetch_url( 'vipgoci_curl_headers' ); - curl_setopt( - $ch, - CURLOPT_HTTPHEADER, - array( 'Authorization: token ' . $github_token ) - ); + if ( is_string( $github_token ) ) { + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( 'Authorization: token ' . $github_token ) + ); + } + + else if ( is_array( $github_token ) ) { + if ( + ( isset( $github_token[ 'oauth_consumer_key' ] ) ) && + ( isset( $github_token[ 'oauth_consumer_secret' ] ) ) && + ( isset( $github_token[ 'oauth_token' ] ) ) && + ( isset( $github_token[ 'oauth_token_secret' ] ) ) + ) { + $github_auth_header = vipgoci_oauth1_headers_get( + 'GET', + $github_url, + $github_token + ); + + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( + 'Authorization: ' . + $github_auth_header + ) + ); + } + } + // Make sure to pause between GitHub-requests vipgoci_github_wait(); @@ -450,13 +653,13 @@ function vipgoci_github_fetch_url( * record of how long time it took, + and also keep count of how many we do. */ - vipgoci_runtime_measure( 'start', 'github_api' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'github_api_get' ); - vipgoci_counter_report( 'do', 'github_api_request_fetch', 1 ); + vipgoci_counter_report( 'do', 'github_api_request_get', 1 ); $resp_data = curl_exec( $ch ); - vipgoci_runtime_measure( 'stop', 'github_api' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'github_api_get' ); $resp_headers = vipgoci_curl_headers( @@ -518,6 +721,201 @@ function vipgoci_github_fetch_url( return $resp_data; } +/* + * Submit PUT request to the GitHub API. + */ +function vipgoci_github_put_url( + $github_url, + $github_postfields, + $github_token +) { + /* + * Actually send a request to GitHub -- make sure + * to retry if something fails. + */ + do { + /* + * By default, assume request went through okay. + */ + + $ret_val = 0; + + /* + * By default, do not retry the request, + * just assume everything goes well + */ + + $retry_req = false; + + /* + * Initialize and send request. + */ + + $ch = curl_init(); + + curl_setopt( + $ch, CURLOPT_URL, $github_url + ); + + curl_setopt( + $ch, CURLOPT_RETURNTRANSFER, 1 + ); + + curl_setopt( + $ch, CURLOPT_CONNECTTIMEOUT, 20 + ); + + curl_setopt( + $ch, CURLOPT_USERAGENT, VIPGOCI_CLIENT_ID + ); + + curl_setopt( + $ch, CURLOPT_CUSTOMREQUEST, 'PUT' + ); + + curl_setopt( + $ch, + CURLOPT_POSTFIELDS, + json_encode( $github_postfields ) + ); + + curl_setopt( + $ch, + CURLOPT_HEADERFUNCTION, + 'vipgoci_curl_headers' + ); + + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( 'Authorization: token ' . $github_token ) + ); + + // Make sure to pause between GitHub-requests + vipgoci_github_wait(); + + /* + * Execute query to GitHub, keep + * record of how long time it took, + * and keep count of how many requests we do. + */ + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'github_api_put' ); + + vipgoci_counter_report( 'do', 'github_api_request_put', 1 ); + + $resp_data = curl_exec( $ch ); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'github_api_put' ); + + + $resp_headers = vipgoci_curl_headers( + null, + null + ); + + + /* + * Assume 200 for success, everything else for failure. + */ + if ( intval( $resp_headers['status'][0] ) !== 200 ) { + /* + * Set default wait period between requests + */ + $retry_sleep = 10; + + /* + * Set error-return value + */ + $ret_val = -1; + + /* + * Figure out if to retry... + */ + + // Decode JSON + $resp_data = json_decode( $resp_data ); + + if ( + ( isset( + $resp_headers['retry-after'] + ) ) && + ( intval( + $resp_headers['retry-after'] + ) > 0 ) + ) { + $retry_req = true; + $retry_sleep = intval( + $resp_headers['retry-after'] + ); + } + + else if ( + ( $resp_data->message == + 'Validation Failed' ) && + + ( $resp_data->errors[0] == + 'was submitted too quickly ' . + 'after a previous comment' ) + ) { + /* + * These messages are due to the + * submission being categorized + * as a spam by GitHub -- no good + * reason to retry, really. + */ + $retry_req = false; + $retry_sleep = 20; + } + + else if ( + ( $resp_data->message == + 'Validation Failed' ) + ) { + $retry_req = false; + } + + else if ( + ( $resp_data->message == + 'Server Error' ) + ) { + $retry_req = false; + } + + vipgoci_log( + 'GitHub reported an error' . + ( $retry_req === true ? + ' will retry request in ' . + $retry_sleep . ' seconds' : + '' ), + array( + 'http_url' + => $github_url, + + 'http_response_headers' + => $resp_headers, + + 'http_reponse_body' + => $resp_data, + ) + ); + + sleep( $retry_sleep + 1 ); + } + + vipgoci_github_rate_limits_check( + $github_url, + $resp_headers + ); + + + curl_close( $ch ); + + } while ( $retry_req == true ); + + return $ret_val; +} + /* * Fetch information from GitHub on a particular * commit within a particular repository, using @@ -677,13 +1075,14 @@ function vipgoci_github_fetch_commit_info( * return false on an error. */ function vipgoci_github_pr_reviews_comments_get( - &$prs_comments, - $repo_owner, - $repo_name, + $options, $commit_id, $commit_made_at, - $github_token + &$prs_comments ) { + $repo_owner = $options['repo-owner']; + $repo_name = $options['repo-name']; + $github_token = $options['token']; /* * Try to get comments from cache @@ -721,15 +1120,6 @@ function vipgoci_github_pr_reviews_comments_get( $per_page = 100; $prs_comments_cache = array(); - /* - * FIXME: - * - * Asking for all the pages from GitHub - * might get expensive as we process more - * commits/hour -- maybe cache this in memcache, - * making it possible to share data between processes. - */ - do { $github_url = VIPGOCI_GITHUB_BASE_URL . '/' . @@ -786,14 +1176,174 @@ function vipgoci_github_pr_reviews_comments_get( * can easily be found. */ - $prs_comments[ - $pr_comment->path . ':' . - $pr_comment->position - ][] = $pr_comment; - } + $prs_comments[ + $pr_comment->path . ':' . + $pr_comment->position + ][] = $pr_comment; + } +} + + +/* + * Get all review-comments submitted to a + * particular Pull-Request. + * Supports filtering by: + * - User submitted (parameter: login) + * - Comment state (parameter: comments_active, true/false) + * + * Note that parameter login can be assigned a magic + * value, 'myself', in which case the actual username + * will be assumed to be that of the token-holder. + + */ +function vipgoci_github_pr_reviews_comments_get_by_pr( + $options, + $pr_number, + $filter = array() +) { + + /* + * Calculate caching ID. + * + * Note that $filter should be used here and not its + * individual components, to enable new data to be fetched + * (i.e. avoiding of caching by callers). + */ + $cache_id = array( + __FUNCTION__, $options['repo-owner'], $options['repo-name'], + $pr_number, $filter + ); + + /* + * Try to get cached data + */ + $cached_data = vipgoci_cache( $cache_id ); + + vipgoci_log( + 'Fetching all review comments submitted to a Pull-Request' . + (( $cached_data !== false ) ? ' (cached)' : '' ), + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'pr_number' => $pr_number, + 'filter' => $filter, + ) + ); + + /* + * If we have the information cached, + * return that. + */ + if ( false !== $cached_data ) { + return $cached_data; + } + + if ( + ( isset( $filter['login'] ) ) && + ( 'myself' === $filter['login'] ) + ) { + /* Get info about token-holder */ + $current_user_info = vipgoci_github_authenticated_user_get( + $options['token'] + ); + + $filter['login'] = $current_user_info->login; + } + + $page = 1; + $per_page = 100; + + $all_comments = array(); + + do { + $github_url = + VIPGOCI_GITHUB_BASE_URL . '/' . + 'repos/' . + rawurlencode( $options['repo-owner'] ) . '/' . + rawurlencode( $options['repo-name'] ) . '/' . + 'pulls/' . + rawurlencode( $pr_number ) . '/' . + 'comments?' . + 'page=' . rawurlencode( $page ) . '&' . + 'per_page=' . rawurlencode( $per_page ); + + $comments = json_decode( + vipgoci_github_fetch_url( + $github_url, + $options['token'] + ) + ); + + foreach( $comments as $comment ) { + if ( + ( isset( $filter['login'] ) ) && + ( $comment->user->login !== $filter['login'] ) + ) { + continue; + } + + if ( isset( $filter['comments_active'] ) ) { + if ( + ( ( $comment->position !== null ) && + ( $filter['comments_active'] === false ) ) + || + ( ( $comment->position === null ) && + ( $filter['comments_active'] === true ) ) + ) { + continue; + } + } + + $all_comments[] = $comment; + } + + $page++; + } while( count( $comments ) >= $per_page ); + + /* + * Cache the results and return + */ + vipgoci_cache( $cache_id, $all_comments ); + + return $all_comments; } +/* + * Remove a particular comment. + */ + +function vipgoci_github_pr_reviews_comments_delete( + $options, + $comment_id +) { + vipgoci_log( + 'Deleting an inline comment from a Pull-Request ' . + 'Review', + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'comment_id' => $comment_id, + ) + ); + + $github_url = + VIPGOCI_GITHUB_BASE_URL . '/' . + 'repos/' . + rawurlencode( $options['repo-owner'] ) . '/' . + rawurlencode( $options['repo-name'] ) . '/' . + 'pulls/' . + 'comments/' . + rawurlencode( $comment_id ); + + vipgoci_github_post_url( + $github_url, + array(), + $options['token'], + true // Indicates a 'DELETE' request + ); +} + /* * Get all generic comments made to a Pull-Request from Github. */ @@ -892,7 +1442,7 @@ function vipgoci_github_pr_generic_comment_submit( $dry_run ) { $stats_types_to_process = array( - 'lint', + VIPGOCI_STATS_LINT, ); @@ -1085,7 +1635,9 @@ function vipgoci_github_pr_comments_error_msg( 'commit_id' => $commit_id, 'pr_number' => $pr_number, 'message' => $message, - ) + ), + 0, + true // Log to IRC as well ); $github_url = @@ -1116,6 +1668,9 @@ function vipgoci_github_pr_comments_error_msg( /* * Remove any comments made by us earlier. + * + * FIXME: For future alterations, move comments + * to be removed to arguments of this function. */ function vipgoci_github_pr_comments_cleanup( @@ -1185,23 +1740,21 @@ function vipgoci_github_pr_comments_cleanup( ( strpos( $pr_comment->body, VIPGOCI_SYNTAX_ERROR_STR - ) === false ) - && + ) !== false ) + || ( strpos( $pr_comment->body, VIPGOCI_GITHUB_ERROR_STR - ) === false ) + ) !== false ) ) { - continue; + // Actually delete the comment + vipgoci_github_pr_generic_comment_delete( + $repo_owner, + $repo_name, + $github_token, + $pr_comment->id + ); } - - // Actually delete the comment - vipgoci_github_pr_generic_comment_delete( - $repo_owner, - $repo_name, - $github_token, - $pr_comment->id - ); } } } @@ -1248,6 +1801,124 @@ function vipgoci_github_pr_generic_comment_delete( ); } +/* + * Get all reviews for a particular Pull-Request, + * and allow filtering by: + * - User submitted (parameter: login) + * - State of review (parameter: state, values are: CHANGES_REQUESTED, COMMENTED, APPROVED) + * + * Note that parameter login can be assigned a magic + * value, 'myself', in which case the actual username + * will be assumed to be that of the token-holder. + */ +function vipgoci_github_pr_reviews_get( + $repo_owner, + $repo_name, + $pr_number, + $github_token, + $filter = array() +) { + vipgoci_log( + 'Fetching reviews for Pull-Request', + array( + 'repo_owner' => $repo_owner, + 'repo_name' => $repo_name, + 'pr_number' => $pr_number, + ) + ); + + $ret_reviews = array(); + + $page = 1; + $per_page = 100; + + + /* + * Figure out login name. + */ + if ( + ( ! empty( $filter['login'] ) ) && + ( $filter['login'] === 'myself' ) + ) { + $current_user_info = vipgoci_github_authenticated_user_get( + $github_token + ); + + $filter['login'] = $current_user_info->login; + } + + /* + * Fetch reviews, paged, from GitHub. + */ + + do { + $github_url = + VIPGOCI_GITHUB_BASE_URL . '/' . + 'repos/' . + rawurlencode( $repo_owner ) . '/' . + rawurlencode( $repo_name ) . '/' . + 'pulls/' . + rawurlencode( $pr_number ) . '/' . + 'reviews' . + '?per_page=' . rawurlencode( $per_page ) . '&' . + 'page=' . rawurlencode( $page ); + + + /* + * Fetch reviews, decode result. + */ + $pr_reviews = json_decode( + vipgoci_github_fetch_url( + $github_url, + $github_token + ) + ); + + + /* + * Loop through each review-item, + * do filtering and save the ones + * we want to keep. + */ + + foreach( $pr_reviews as $pr_review ) { + if ( ! empty( $filter['login'] ) ) { + if ( + $pr_review->user->login !== + $filter['login'] + ) { + continue; + } + } + + if ( ! empty( $filter['state'] ) ) { + $match = false; + + foreach( + $filter['state'] as + $allowed_state + ) { + if ( + $pr_review->state === + $allowed_state + ) { + $match = true; + } + } + + if ( false === $match ) { + continue; + } + } + + $ret_reviews[] = $pr_review; + } + + $page++; + } while( count( $pr_reviews ) >= $per_page ); + + return $ret_reviews; +} /* * Submit a review on GitHub for a particular commit, @@ -1265,7 +1936,8 @@ function vipgoci_github_pr_review_submit( ) { $stats_types_to_process = array( - 'phpcs', + VIPGOCI_STATS_PHPCS, + VIPGOCI_STATS_HASHES_API, ); vipgoci_log( @@ -1370,7 +2042,7 @@ function vipgoci_github_pr_review_submit( * If there are any 'error'-level issues, make sure the submission * asks for changes to be made, otherwise only comment. * - * If there are no issues at all -- warning or error -- do not + * If there are no issues at all -- warning, error, info -- do not * submit anything. */ @@ -1378,6 +2050,7 @@ function vipgoci_github_pr_review_submit( $github_errors = false; $github_warnings = false; + $github_info = false; foreach ( $stats_types_to_process as @@ -1397,6 +2070,13 @@ function vipgoci_github_pr_review_submit( ) ) { $github_warnings = true; } + + if ( ! empty( + $results['stats'] + [ $stats_type ][ $pr_number ]['info'] + ) ) { + $github_info = true; + } } @@ -1407,7 +2087,8 @@ function vipgoci_github_pr_review_submit( */ if ( ( false === $github_errors ) && - ( false === $github_warnings ) + ( false === $github_warnings ) && + ( false === $github_info ) ) { continue; } @@ -1451,7 +2132,6 @@ function vipgoci_github_pr_review_submit( continue; } - $github_postfields['body'] .= '**' . $stats_type . '**' . " scanning turned up:\n\r"; @@ -1465,32 +2145,44 @@ function vipgoci_github_pr_review_submit( $commit_issue_stat_key => $commit_issue_stat_value ) { + /* + * Do not include statistic in the + * the report if nothing is found. + * + * Note that if nothing is found at + * all, we will not get to this point, + * so there is no need to report if + * nothing is found at all. + */ + if ( 0 === $commit_issue_stat_value ) { + continue; + } + $github_postfields['body'] .= vipgoci_github_labels( $commit_issue_stat_key ) . ' ' . $commit_issue_stat_value . ' ' . - $commit_issue_stat_key . - ( $commit_issue_stat_value > 1 ) ? '' :'s' . + $commit_issue_stat_key . + ( ( $commit_issue_stat_value > 1 ) ? 's' : '' ) . ' ' . "\n\r"; } + } - - /* - * If we have a informational-URL about - * the bot, append it along with a generic - * message. - */ - if ( null !== $informational_url ) { - $github_postfields['body'] .= - "\n\r***\n\r" . - sprintf( - VIPGOCI_INFORMATIONAL_MESSAGE, - $informational_url - ); - } + /* + * If we have a informational-URL about + * the bot, append it along with a generic + * message. + */ + if ( null !== $informational_url ) { + $github_postfields['body'] .= + "\n\r***\n\r" . + sprintf( + VIPGOCI_INFORMATIONAL_MESSAGE, + $informational_url + ); } @@ -1593,6 +2285,199 @@ function vipgoci_github_pr_review_submit( return; } +/* + * Dismiss a particular review + * previously submitted to a Pull-Request. + */ + +function vipgoci_github_pr_review_dismiss( + $repo_owner, + $repo_name, + $pr_number, + $review_id, + $dismiss_message, + $github_token +) { + + vipgoci_log( + 'Dismissing a Pull-Request Review', + array( + 'repo_owner' => $repo_owner, + 'repo_name' => $repo_name, + 'pr_number' => $pr_number, + 'review_id' => $review_id, + 'dismiss_message' => $dismiss_message, + ) + ); + + $github_url = + VIPGOCI_GITHUB_BASE_URL . '/' . + 'repos/' . + rawurlencode( $repo_owner ) . '/' . + rawurlencode( $repo_name ) . '/' . + 'pulls/' . + rawurlencode( $pr_number ) . '/' . + 'reviews/' . + rawurlencode( $review_id ) . '/' . + 'dismissals'; + + vipgoci_github_put_url( + $github_url, + array( + 'message' => $dismiss_message + ), + $github_token + ); +} + + +/* + * Dismiss all Pull-Request Reviews that have no + * active comments attached to them. + */ +function vipgoci_github_pr_reviews_dismiss_non_active_comments( + $options, + $pr_number +) { + vipgoci_log( + 'Dismissing any Pull-Request reviews submitted by ' . + 'us and contain no active inline comments any more', + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'pr_number' => $pr_number, + ) + ); + + /* + * Get any Pull-Request reviews with changes + * required status, and submitted by us. + */ + $pr_reviews = vipgoci_github_pr_reviews_get( + $options['repo-owner'], + $options['repo-name'], + $pr_number, + $options['token'], + array( + 'login' => 'myself', + 'state' => array( 'CHANGES_REQUESTED' ) + ) + ); + + /* + * Get all comments to a the current Pull-Request. + * + * Note that we must bypass cache here, + */ + $all_comments = vipgoci_github_pr_reviews_comments_get_by_pr( + $options, + $pr_number, + array( + 'login' => 'myself', + 'timestamp' => time() // To bypass caching + ) + ); + + if ( count( $all_comments ) === 0 ) { + /* + * In case we receive no comments at all + * from GitHub, do not do anything, as a precaution. + * Receiving no comments might indicate a + * failure (communication error or something else), + * and if we dismiss reviews that seem not to + * contain any comments, we might risk dismissing + * all reviews when there is a failure. By + * doing this, we take much less risk. + */ + vipgoci_log( + 'Not dismissing any reviews, as no inactive ' . + 'comments submitted to the Pull-Request ' . + 'were found', + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'pr_number' => $pr_number, + ) + ); + + return; + } + + $reviews_status = array(); + + foreach( $all_comments as $comment_item ) { + /* + * Not associated with a review? Ignore then. + */ + if ( ! isset( $comment_item->pull_request_review_id ) ) { + continue; + } + + /* + * If the review ID is not found in + * the array of reviews, put in 'null'. + */ + if ( ! isset( $reviews_status[ + $comment_item->pull_request_review_id + ] ) ) { + $reviews_status[ + $comment_item->pull_request_review_id + ] = null; + } + + /* + * In case position (relative line number) + * is at null, this means that the comment + * is no longer 'active': It has become obsolete + * as the code has changed. If we have not so far + * found any instance of the review associated + * with the comment having other active comments, + * mark it as 'safe to dismiss'. + */ + if ( null === $comment_item->position ) { + if ( + $reviews_status[ + $comment_item->pull_request_review_id + ] !== false + ) { + $reviews_status[ + $comment_item->pull_request_review_id + ] = true; + } + } + + else { + $reviews_status[ + $comment_item->pull_request_review_id + ] = false; + } + } + + /* + * Loop through each review we + * found matching the specific criteria. + */ + foreach( $pr_reviews as $pr_review ) { + /* + * If no active comments were found, + * it should be safe to dismiss the review. + */ + if ( + ( isset( $reviews_status[ $pr_review->id ] ) ) && + ( true === $reviews_status[ $pr_review->id ] ) + ) { + vipgoci_github_pr_review_dismiss( + $options['repo-owner'], + $options['repo-name'], + $pr_number, + $pr_review->id, + 'Dismissing review as all inline comments ' . + 'are obsolete by now', + $options['token'] + ); + } + } +} /* * Approve a Pull-Request, and afterwards @@ -1603,7 +2488,7 @@ function vipgoci_github_pr_review_submit( * * The race-conditions can occur when a Pull-Request * is approved, but it is approved after a new commit - * was added, but that has not been scanned. + * was added which has not been scanned. */ function vipgoci_github_approve_pr( @@ -1612,11 +2497,9 @@ function vipgoci_github_approve_pr( $github_token, $pr_number, $latest_commit_id, - $filetypes_approve, + $message, $dry_run ) { - - $github_url = VIPGOCI_GITHUB_BASE_URL . '/' . 'repos/' . @@ -1628,14 +2511,13 @@ function vipgoci_github_approve_pr( $github_postfields = array( 'commit_id' => $latest_commit_id, - 'body' => 'Auto-approved Pull-Request #' . - (int) $pr_number . ' as it ' . - 'contains only allowable file-types ' . - '(' . implode( ', ', $filetypes_approve ) . ')', + 'body' => null, 'event' => 'APPROVE', 'comments' => array() ); + $github_postfields['body'] = $message; + if ( true === $dry_run ) { return; } @@ -1887,7 +2769,8 @@ function vipgoci_github_pr_files_changed( $repo_name, $github_token, $pr_base_sha, - $current_commit_id + $current_commit_id, + true ); $files_changed_ret = array(); @@ -1917,7 +2800,8 @@ function vipgoci_github_diffs_fetch( $repo_name, $github_token, $commit_id_a, - $commit_id_b + $commit_id_b, + $empty_patches_also = false ) { /* @@ -1978,10 +2862,17 @@ function vipgoci_github_diffs_fetch( * Loop through all files, save patch in an array */ foreach( $resp_raw->files as $file_item ) { - if ( ! isset( $file_item->patch ) ) { + if ( + ( false === $empty_patches_also ) && + ( ! isset( $file_item->patch ) ) + ) { continue; } + if ( ! isset( $file_item->patch ) ) { + $file_item->patch = null; + } + $diffs[ $file_item->filename ] = $file_item->patch; } @@ -2018,13 +2909,36 @@ function vipgoci_github_authenticated_user_get( $github_token ) { VIPGOCI_GITHUB_BASE_URL . '/' . 'user'; - $current_user_info = json_decode( - vipgoci_github_fetch_url( - $github_url, - $github_token - ) + $current_user_info_json = vipgoci_github_fetch_url( + $github_url, + $github_token ); + $current_user_info = null; + + if ( false !== $current_user_info_json ) { + $current_user_info = json_decode( + $current_user_info_json + ); + } + + if ( + ( false === $current_user_info_json ) || + ( null === $current_user_info ) + ) { + vipgoci_log( + 'Unable to get information about token-holder from' . + 'GitHub due to error', + array( + 'current_user_info_json' => $current_user_info_json, + 'current_user_info' => $current_user_info, + ) + ); + + return false; + } + + vipgoci_cache( $cached_id, $current_user_info ); return $current_user_info; @@ -2103,7 +3017,7 @@ function vipgoci_github_labels_get( */ $cache_id = array( __FUNCTION__, $repo_owner, $repo_name, - $github_token, $pr_number, $label_to_look_for + $github_token, $pr_number, $label_to_look_for ); $cached_data = vipgoci_cache( $cache_id ); diff --git a/latest-release.php b/latest-release.php index 38573529e..92d81d4c0 100755 --- a/latest-release.php +++ b/latest-release.php @@ -12,10 +12,10 @@ $ch = curl_init(); -curl_setopt( $ch, CURLOPT_URL, $github_url ); -curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); -curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 20 ); -curl_setopt( $ch, CURLOPT_USERAGENT, $client_id ); +curl_setopt( $ch, CURLOPT_URL, $github_url ); +curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); +curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 20 ); +curl_setopt( $ch, CURLOPT_USERAGENT, $client_id ); $resp_data_raw = curl_exec( $ch ); @@ -31,7 +31,7 @@ $resp_data = json_decode( - $resp_data_raw + $resp_data_raw ); unset( $resp_data_raw ); diff --git a/lint-scan.php b/lint-scan.php index 06e50e6a6..bcbcb255a 100644 --- a/lint-scan.php +++ b/lint-scan.php @@ -36,11 +36,11 @@ function vipgoci_lint_do_scan( * measure how long time it took */ - vipgoci_runtime_measure( 'start', 'php_lint_cli' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'php_lint_cli' ); exec( $cmd, $file_issues_arr ); - vipgoci_runtime_measure( 'stop', 'php_lint_cli' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'php_lint_cli' ); vipgoci_log( @@ -154,7 +154,7 @@ function vipgoci_lint_scan_commit( $commit_id = $options['commit']; $github_token = $options['token']; - vipgoci_runtime_measure( 'start', 'lint_scan_commit' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'lint_scan_commit' ); vipgoci_log( 'About to lint PHP-files', @@ -209,7 +209,7 @@ function vipgoci_lint_scan_commit( */ foreach( $commit_tree as $filename ) { - vipgoci_runtime_measure( 'start', 'lint_scan_single_file' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'lint_scan_single_file' ); $file_contents = vipgoci_gitrepo_fetch_committed_file( $repo_owner, @@ -218,7 +218,7 @@ function vipgoci_lint_scan_commit( $commit_id, $filename, $options['local-git-repo'] - ); + ); // Save the file-contents in a temporary-file $temp_file_name = vipgoci_save_temp_file( @@ -281,7 +281,7 @@ function vipgoci_lint_scan_commit( // If there are no new issues, just leave it at that if ( empty( $file_issues_arr ) ) { - vipgoci_runtime_measure( 'stop', 'lint_scan_single_file' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'lint_scan_single_file' ); continue; } @@ -332,7 +332,7 @@ function vipgoci_lint_scan_commit( $commit_issues_submit[ $pr_item->number ][] = array( - 'type' => 'lint', + 'type' => VIPGOCI_STATS_LINT, 'file_name' => $filename, @@ -352,7 +352,7 @@ function vipgoci_lint_scan_commit( } } - vipgoci_runtime_measure( 'stop', 'lint_scan_single_file' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'lint_scan_single_file' ); } @@ -369,5 +369,5 @@ function vipgoci_lint_scan_commit( gc_collect_cycles(); - vipgoci_runtime_measure( 'stop', 'lint_scan_commit' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'lint_scan_commit' ); } diff --git a/misc.php b/misc.php index 44d32c933..eefb17fdc 100644 --- a/misc.php +++ b/misc.php @@ -1,23 +1,17 @@ $value ) { @@ -509,7 +544,14 @@ function vipgoci_stats_init( $options, $prs_implicated, &$results ) { ); } - foreach ( array( 'phpcs', 'lint' ) as $stats_type ) { + foreach ( + array( + VIPGOCI_STATS_PHPCS, + VIPGOCI_STATS_LINT, + VIPGOCI_STATS_HASHES_API + ) + as $stats_type + ) { /* * Initialize stats for the stats-types only when * supposed to run them @@ -524,7 +566,8 @@ function vipgoci_stats_init( $options, $prs_implicated, &$results ) { $results['stats'][ $stats_type ] [ $pr_item->number ] = array( 'error' => 0, - 'warning' => 0 + 'warning' => 0, + 'info' => 0, ); } } @@ -551,15 +594,15 @@ function vipgoci_runtime_measure( $action = null, $type = null ) { * Check usage. */ if ( - ( 'start' !== $action ) && - ( 'stop' !== $action ) && - ( 'dump' !== $action ) + ( VIPGOCI_RUNTIME_START !== $action ) && + ( VIPGOCI_RUNTIME_STOP !== $action ) && + ( VIPGOCI_RUNTIME_DUMP !== $action ) ) { return false; } // Dump all runtimes we have - if ( 'dump' === $action ) { + if ( VIPGOCI_RUNTIME_DUMP === $action ) { return $runtime; } @@ -574,13 +617,13 @@ function vipgoci_runtime_measure( $action = null, $type = null ) { } - if ( 'start' === $action ) { + if ( VIPGOCI_RUNTIME_START === $action ) { $timers[ $type ] = microtime( true ); return true; } - else if ( 'stop' === $action ) { + else if ( VIPGOCI_RUNTIME_STOP === $action ) { if ( ! isset( $timers[ $type ] ) ) { return false; } @@ -726,7 +769,7 @@ function vipgoci_github_comment_match( * as "Warning: ..." -- remove all of that. */ $comment_made_body = str_replace( - array("**", "Warning", "Error", ":no_entry_sign:", ":exclamation:"), + array("**", "Warning", "Error", "Info", ":no_entry_sign:", ":exclamation:", ":information_source:"), array("", "", "", "", ""), $comment_made->body ); @@ -740,9 +783,33 @@ function vipgoci_github_comment_match( ': ' ); + /* + * Transform strings to lowercase. + */ + $comment_made_body = strtolower( + $comment_made_body + ); + + $file_issue_comment = strtolower( + $file_issue_comment + ); + + /* + * Check if comments match, including + * if we need to HTML-encode our new comment + * (GitHub encodes their comments when + * returning them. + */ if ( - strtolower( $comment_made_body ) == - strtolower( $file_issue_comment ) + ( + $comment_made_body == + $file_issue_comment + ) + || + ( + $comment_made_body == + htmlentities( $file_issue_comment ) + ) ) { /* Comment found, return true. */ return true; @@ -752,3 +819,615 @@ function vipgoci_github_comment_match( return false; } +/* + * Remove comments that exist on a GitHub Pull-Request from + * the results array. Will loop through each Pull-Request + * affected by the current commit, and remove any comment + * from the results array if it already exists. + */ +function vipgoci_remove_existing_github_comments_from_results( + $options, + $prs_implicated, + &$results, + $ignore_dismissed_reviews = false +) { + vipgoci_log( + 'Removing existing GitHub comments from results' . + ' to be posted to GitHub API', + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'prs_implicated' => array_keys( $prs_implicated ), + 'ignore_dismissed_reviews' => $ignore_dismissed_reviews, + ) + ); + + $comments_removed = array(); + + foreach ( $prs_implicated as $pr_item ) { + $prs_comments = array(); + + if ( ! isset( + $comments_removed[ $pr_item->number ] + ) ) { + $comments_removed[ $pr_item->number ] = array(); + } + + /* + * Get all commits related to the current + * Pull-Request. + */ + + $pr_item_commits = vipgoci_github_prs_commits_list( + $options['repo-owner'], + $options['repo-name'], + $pr_item->number, + $options['token'] + ); + + /* + * Loop through each commit, fetching all comments + * made in relation to that commit + */ + + foreach ( $pr_item_commits as $pr_item_commit_id ) { + vipgoci_github_pr_reviews_comments_get( + $options, + $pr_item_commit_id, + $pr_item->created_at, + $prs_comments + ); + + unset( $pr_item_commit_id ); + } + + + /* + * Ignore dismissed reviews, if requested. + */ + if ( true === $ignore_dismissed_reviews ) { + /* + * Get dismissed reviews and extract ID of each. + */ + $pr_reviews = vipgoci_github_pr_reviews_get( + $options['repo-owner'], + $options['repo-name'], + $pr_item->number, + $options['token'], + array( + 'login' => 'myself', + 'state' => array( 'DISMISSED' ) + ) + ); + + $dismissed_reviews = array_column( + $pr_reviews, + 'id' + ); + + unset( $pr_reviews ); + + + /* + * Loop through each file to have comments + * submitted against, then look through each + * comment, looking for any comment associated + * with dismissed reviews. + * + * If we find a dismissed review, we will act + * as if the comment was never there by removing + * it from $prs_comments. This will ensure + * that our to-be posted review will contain + * such comments, even though they could be + * considered duplictes. The aim is to make + * them more visible and part of a blocking review. + */ + + $removed_comments = array(); + + foreach( + $prs_comments as + $pr_comment_key => $pr_comments_items + ) { + foreach( + $pr_comments_items as + $pr_review_key => $pr_review_comment + ) { + if ( false === in_array( + $pr_review_comment->pull_request_review_id, + $dismissed_reviews + ) ) { + continue; + } + + $removed_comments[] = array( + 'pr_number' => + $pr_item->number, + + 'pull_request_review_id' => + $pr_review_comment->pull_request_review_id, + + 'comment_id' => + $pr_review_comment->id, + + 'message_body' => + $pr_review_comment->body, + + 'message_created_at' => + $pr_review_comment->created_at, + + 'message_updated_at' => + $pr_review_comment->updated_at, + ); + + + /* + * Comment is a part of a dismissed review, + * get rid of the comment -- act as if was + * never there. + */ + unset( + $prs_comments[ + $pr_comment_key + ][ + $pr_review_key + ] + ); + } + } + + vipgoci_log( + 'Removed following comments from list of previously submitted ' . + 'comments to older PR reviews, as they are ' . + 'part of dismissed reviews', + + array( + 'removed_comments' => + $removed_comments + ) + ); + + unset( $removed_comments ); + unset( $dismissed_reviews ); + } + + + foreach( + $results['issues'][ $pr_item->number ] as + $tobe_submitted_cmt_key => + $tobe_submitted_cmt + ) { + + /* + * Filter out issues that have already been + * reported to GitHub. + */ + + if ( + // Only do check if everything above is looking good + vipgoci_github_comment_match( + $tobe_submitted_cmt['file_name'], + $tobe_submitted_cmt['file_line'], + $tobe_submitted_cmt['issue']['message'], + $prs_comments + ) + ) { + /* + * Keep a record of what we remove. + */ + $comments_removed[ $pr_item->number ][] = + $tobe_submitted_cmt; + + /* Remove it */ + unset( + $results[ + 'issues' + ][ + $pr_item->number + ][ + $tobe_submitted_cmt_key + ] + ); + + /* + * Update statistics + */ + $results[ + 'stats' + ][ + $tobe_submitted_cmt['type'] + ][ + $pr_item->number + ][ + strtolower( + $tobe_submitted_cmt['issue']['type'] + ) + ]--; + } + } + + /* + * Re-create the issues + * array, so that no array + * keys are missing. + */ + $results[ + 'issues' + ][ + $pr_item->number + ] = array_values( + $results[ + 'issues' + ][ + $pr_item->number + ] + ); + } + + /* + * Report what we removed. + */ + vipgoci_log( + 'Removed following comments from array of ' . + 'to be submitted comments to PRs, as they ' . + 'have been submitted already', + array( + 'comments_removed' => $comments_removed + ) + ); +} + +/* + * For each approved file, remove any issues + * to be submitted against them. However, + * do not do this for 'info' type messages, + * as they are informational, and not problems. + * + * We do this, because sometimes Pull-Requests + * will be opened that contain approved code, + * and we do not want to clutter them with + * non-relevant comments. + * + * Make sure to update statistics to + * reflect this. + */ + +function vipgoci_approved_files_comments_remove( + $options, + &$results, + $auto_approved_files_arr +) { + + $issues_removed = array( + ); + + vipgoci_log( + 'Removing any potential issues (errors, warnings) ' . + 'found for approved files from internal results', + + array( + 'auto_approved_files_arr' => $auto_approved_files_arr, + ) + ); + + /* + * Loop through each Pull-Request + */ + foreach( $results['issues'] as + $pr_number => $pr_issues + ) { + /* + * Loop through each issue affecting each + * Pull-Request. + */ + foreach( $pr_issues as + $issue_number => $issue_item + ) { + + /* + * If the file affected is + * not found in the auto-approved files, + * do not to anything. + */ + if ( ! isset( + $auto_approved_files_arr[ + $issue_item['file_name'] + ] + ) ) { + continue; + } + + /* + * We do not touch on 'info' type, + * as that does not report any errors. + */ + + if ( strtolower( + $issue_item['issue']['type'] + ) === 'info' ) { + continue; + } + + /* + * We have found an item that is approved, + * and has non-info issues -- remove it + * from the array of submittable issues. + */ + unset( + $results[ + 'issues' + ][ + $pr_number + ][ + $issue_number + ] + ); + + /* + * Update statistics accordingly. + */ + $results[ + 'stats' + ][ + $issue_item['type'] + ][ + $pr_number + ][ + strtolower( + $issue_item['issue']['type'] + ) + ]--; + + /* + * Update our own information array on + * what we did. + */ + $issues_removed[ + $pr_number + ][] = $issue_item; + } + + /* + * Re-order the array as + * some keys might be missing + */ + $results[ + 'issues' + ][ + $pr_number + ] = array_values( + $results[ + 'issues' + ][ + $pr_number + ] + ); + } + + + vipgoci_log( + 'Completed cleaning out issues for pre-approved files', + array( + 'issues_removed' => $issues_removed, + ) + ); +} + +/* + * Limit the number of to-be-submitted comments to + * the Pull-Requests. We take into account the number + * to be submitted for each Pull-Request, the number of + * comments already submitted, and the limit specified + * on start-up. Comments are removed as needed, and + * what comments are removed is reported. + */ +function vipgoci_github_results_filter_comments_to_max( + $options, + &$results +) { + + vipgoci_log( + 'Preparing to remove any excessive number comments from array of ' . + 'issues to be submitted to PRs', + array( + 'review_comments_total_max' + => $options['review-comments-total-max'], + ) + ); + + + /* + * We might need to remove comments. + * + * We will begin with lower priority comments + * first, remove them, and then progressively + * continue removing comments as priority increases + * and there is still a need for removal. + */ + + /* + * Keep track of what we remove. + */ + $comments_removed = array(); + + foreach( + $results['issues'] as + $pr_number => $pr_issues_comments + ) { + /* + * Take into account previously submitted comments + * by us for the current Pull-Request. + */ + + $pr_previous_comments_cnt = count( + vipgoci_github_pr_reviews_comments_get_by_pr( + $options, + $pr_number, + array( + 'login' => 'myself', + 'comments_active' => true, + ) + ) + ); + + /* + * How many comments need + * to be removed? Count in + * comments in the PR in addition + * to possible new ones, substract + * from the maximum specified. + */ + + $comments_to_remove = + ( + count( $pr_issues_comments ) + + + $pr_previous_comments_cnt + ) + - + $options['review-comments-total-max']; + + /* + * If there are no comments to remove, + * skip and continue. + */ + if ( $comments_to_remove <= 0 ) { + continue; + } + + /* + * If more are to be removed than are to be + * submitted, limit to the number of available ones. + */ + else if ( + $comments_to_remove > + count( $pr_issues_comments ) + ) { + $comments_to_remove = count( $pr_issues_comments ); + } + + /* + * Figure out severity, minimum and maximum. + */ + + $severity_min = 0; + $severity_max = 0; + + foreach( $pr_issues_comments as $pr_issue ) { + $severity_min = min( + $pr_issue['issue']['severity'], + $severity_min + ); + + $severity_max = max( + $pr_issue['issue']['severity'], + $severity_max + ); + } + + /* + * Loop through severity-levels from low to high + * and remove comments as needed. + */ + for ( + $severity_current = $severity_min; + $severity_current <= $severity_max && + $comments_to_remove > 0; + $severity_current++ + ) { + foreach( + $pr_issues_comments as + $pr_issue_key => $pr_issue + ) { + /* + * If we have removed enough, stop here. + */ + if ( $comments_to_remove <= 0 ) { + break; + } + + /* + * Not correct severity level? Ignore. + */ + if ( + $pr_issue['issue']['severity'] !== + $severity_current + ) { + continue; + } + + /* + * Actually remove and + * keep statistics up to date. + */ + + unset( + $results[ + 'issues' + ][ + $pr_number + ][ + $pr_issue_key + ] + ); + + $results[ + 'stats' + ][ + $pr_issue['type'] + ][ + $pr_number + ][ + strtolower( + $pr_issue['issue']['type'] + ) + ]--; + + /* + * Keep track of what we remove + */ + if ( ! isset( + $comments_removed[ + $pr_number + ] + ) ) { + $comments_removed[ + $pr_number + ] = array(); + } + + $comments_removed[ + $pr_number + ] = $pr_issue; + + $comments_to_remove--; + } + } + + /* + * Re-create array so to + * keep continuous ordering + * of index. + */ + $results[ + 'issues' + ][ + $pr_number + ] = array_values( + $results[ + 'issues' + ][ + $pr_number + ] + ); + } + + vipgoci_log( + 'Removed issue comments from array of to be submitted ' . + 'comments to PRs due to limit constraints', + array( + 'review_comments_total_max' => $options['review-comments-total-max'], + 'comments_removed' => $comments_removed, + ) + ); +} diff --git a/other-web-services.php b/other-web-services.php new file mode 100644 index 000000000..a54a549b9 --- /dev/null +++ b/other-web-services.php @@ -0,0 +1,192 @@ + $msg_queue, + ) + ); + + foreach( $msg_queue as $message ) { + $irc_api_postfields = array( + 'message' => $message, + 'botname' => $botname, + 'channel' => $channel, + ); + + $ch = curl_init(); + + curl_setopt( + $ch, CURLOPT_URL, $irc_api_url + ); + + curl_setopt( + $ch, CURLOPT_RETURNTRANSFER, 1 + ); + + curl_setopt( + $ch, CURLOPT_CONNECTTIMEOUT, 20 + ); + + curl_setopt( + $ch, CURLOPT_USERAGENT, VIPGOCI_CLIENT_ID + ); + + curl_setopt( + $ch, CURLOPT_POST, 1 + ); + + curl_setopt( + $ch, + CURLOPT_POSTFIELDS, + json_encode( $irc_api_postfields ) + ); + + curl_setopt( + $ch, + CURLOPT_HEADERFUNCTION, + 'vipgoci_curl_headers' + ); + + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( 'Authorization: Bearer ' . $irc_api_token ) + ); + + /* + * Execute query, keep record of how long time it + * took, and keep count of how many requests we do. + */ + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'irc_api_post' ); + + vipgoci_counter_report( 'do', 'irc_api_request_post', 1 ); + + $resp_data = curl_exec( $ch ); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'irc_api_post' ); + + $resp_headers = vipgoci_curl_headers( + null, + null + ); + + curl_close( $ch ); + + /* + * Enforce a small wait between requests. + */ + + time_nanosleep( 0, 500000000 ); + } +} + +/* + * Send statistics to pixel API so + * we can keep track of actions we + * take during runtime. + */ +function vipgoci_send_stats_to_pixel_api( + $pixel_api_url, + $statistic_group, + $stat_names_to_report, + $statistics +) { + vipgoci_log( + 'Sending statistics to pixel API service', + array( + 'stat_names_to_report' => + $stat_names_to_report + ) + ); + + foreach( + $statistics as + $stat_name => $stat_value + ) { + /* + * We are to report only certain + * values, so skip those who we should + * not report on. + */ + if ( false === in_array( + $stat_name, + $stat_names_to_report + ) ) { + /* + * Not found, so nothing to report, skip. + */ + continue; + } + + /* + * Compose URL. + */ + $url = + $pixel_api_url . + '?' . + 'v=wpcom-no-pv' . + '&' . + 'x_' . rawurlencode( $statistic_group ) . + '/' . + rawurlencode( + $stat_name + ) . '=' . + rawurlencode( + $stat_value + ); + + /* + * Call service, do nothing with output. + */ + file_get_contents( $url ); + + /* + * Sleep a short while between + * requests. + */ + time_nanosleep( + 0, + 500000000 + ); + } +} + + diff --git a/phpcs-scan.php b/phpcs-scan.php index e3cd4023b..d5c424d0f 100644 --- a/phpcs-scan.php +++ b/phpcs-scan.php @@ -18,27 +18,105 @@ function vipgoci_phpcs_do_scan( * * Make sure to use wide enough output, so we can catch all of it. */ -// FIXME: When dealing with SVG files -// use: --extensions=svg --sniffs=WordPressVIPMinimum.SVG.HTMLCode $cmd = sprintf( - '%s %s --standard=%s --severity=%s --report=%s %s 2>&1', + '%s %s --standard=%s --severity=%s --report=%s', escapeshellcmd( 'php' ), escapeshellcmd( $phpcs_path ), escapeshellarg( $phpcs_standard ), escapeshellarg( $phpcs_severity ), - escapeshellarg( 'json' ), + escapeshellarg( 'json' ) + ); + + /* + * Lastly, append the target filename + * to the command-line string. + */ + $cmd .= sprintf( + ' %s', escapeshellarg( $filename_tmp ) ); - vipgoci_runtime_measure( 'start', 'phpcs_cli' ); + $cmd .= ' 2>&1'; + + vipgoci_log( + 'Running PHPCS now', + array( + 'cmd' => $cmd, + ), + 2 + ); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'phpcs_cli' ); $result = shell_exec( $cmd ); - vipgoci_runtime_measure( 'stop', 'phpcs_cli' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'phpcs_cli' ); return $result; } +function vipgoci_phpcs_scan_single_file( + $options, + $file_name +) { + $file_contents = vipgoci_gitrepo_fetch_committed_file( + $options['repo-owner'], + $options['repo-name'], + $options['token'], + $options['commit'], + $file_name, + $options['local-git-repo'] + ); + + $file_extension = vipgoci_file_extension( + $file_name + ); + + if ( empty( $file_extension ) ) { + $file_extension = null; + } + + $temp_file_name = vipgoci_save_temp_file( + 'phpcs-scan-', + $file_extension, + $file_contents + ); + + vipgoci_log( + 'About to PHPCS-scan file', + array( + 'repo_owner' => $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'commit_id' => $options['commit'], + 'filename' => $file_name, + 'file_extension' => $file_extension, + 'temp_file_name' => $temp_file_name, + ) + ); + + + $file_issues_str = vipgoci_phpcs_do_scan( + $temp_file_name, + $options['phpcs-path'], + $options['phpcs-standard'], + $options['phpcs-severity'] + ); + + /* Get rid of temporary file */ + unlink( $temp_file_name ); + + $file_issues_arr_master = json_decode( + $file_issues_str, + true + ); + + return array( + 'file_issues_arr_master' => $file_issues_arr_master, + 'file_issues_str' => $file_issues_str, + 'temp_file_name' => $temp_file_name, + ); +} + /* * Dump output of scan-analysis to a file, @@ -75,14 +153,10 @@ function vipgoci_phpcs_scan_output_dump( $output_file, $data ) { * that existed prior to the change. */ function vipgoci_issues_filter_irrellevant( - $repo_owner, - $repo_name, - $commit_id, $file_name, $file_issues_arr, $file_blame_log, $pr_item_commits, - $comments_existing, $file_relative_lines ) { /* @@ -141,40 +215,6 @@ function vipgoci_issues_filter_irrellevant( continue; } - /* - * Filter out issues that have already been - * reported got GitHub. - */ - - if ( - // Only do check if everything above is looking good - vipgoci_github_comment_match( - $file_name, - $file_relative_lines[ - $file_issue_val['line'] - ], - $file_issue_val['message'], - $comments_existing - ) - ) { - vipgoci_log( - 'Skipping submission of ' . - 'comment, has already been ' . - 'submitted', - array( - 'repo_owner' => $repo_owner, - 'repo_name' => $repo_name, - 'filename' => $file_name, - 'file_issue_line' => $file_issue_val['line'], - 'file_issue_msg' => $file_issue_val['message'], - 'commit_id' => $commit_id, - ) - ); - - /* Skip */ - continue; - } - // Passed all tests, keep this issue $file_issues_ret[] = $file_issue_val; } @@ -229,7 +269,7 @@ function vipgoci_phpcs_scan_commit( $commit_id = $options['commit']; $github_token = $options['token']; - vipgoci_runtime_measure( 'start', 'phpcs_scan_commit' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'phpcs_scan_commit' ); vipgoci_log( 'About to PHPCS-scan repository', @@ -320,12 +360,23 @@ function vipgoci_phpcs_scan_commit( $commit_id, array( 'file_extensions' => - array( 'php', 'js', 'twig' ), + /* + * If SVG-checks are enabled, + * include it in the file-extensions + */ + array_merge( + array( 'php', 'js', 'twig' ), + ( $options['svg-checks'] ? + array( 'svg' ) : + array() + ) + ), 'skip_folders' => $options['skip-folders'], ) ); + foreach ( $pr_item_files_tmp as $pr_item_file_name ) { if ( in_array( $pr_item_file_name, @@ -361,6 +412,8 @@ function vipgoci_phpcs_scan_commit( 'repo_owner' => $repo_owner, 'repo_name' => $repo_name, 'commit_id' => $commit_id, + 'all_files_changed_by_prs' => + $pr_item_files_changed['all'], ) ); @@ -370,59 +423,41 @@ function vipgoci_phpcs_scan_commit( * Loop through each file affected by * the commit. */ - vipgoci_runtime_measure( 'start', 'phpcs_scan_single_file' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_START, 'phpcs_scan_single_file' ); - $file_contents = vipgoci_gitrepo_fetch_committed_file( - $repo_owner, - $repo_name, - $github_token, - $commit_id, - $file_name, - $options['local-git-repo'] - ); - - $file_extension = pathinfo( - $file_name, - PATHINFO_EXTENSION - ); - - if ( empty( $file_extension ) ) { - $file_extension = null; - } - - $temp_file_name = vipgoci_save_temp_file( - 'phpcs-scan-', - $file_extension, - $file_contents + $file_extension = vipgoci_file_extension( + $file_name ); - vipgoci_log( - 'About to PHPCS-scan file', - array( - 'repo_owner' => $repo_owner, - 'repo_name' => $repo_name, - 'commit_id' => $commit_id, - 'filename' => $file_name, - 'temp_file_name' => $temp_file_name, - ) - ); - - - $file_issues_str = vipgoci_phpcs_do_scan( - $temp_file_name, - $options['phpcs-path'], - $options['phpcs-standard'], - $options['phpcs-severity'] + /* + * If a SVG file, scan using a + * custom internal function, otherwise + * use PHPCS. + * + * However, only do this if SVG-checks + * is enabled. + */ + $scanning_func = + ( + ( 'svg' === $file_extension ) && + ( $options['svg-checks'] ) + ) ? + 'vipgoci_svg_scan_single_file' : + 'vipgoci_phpcs_scan_single_file'; + + $tmp_scanning_results = $scanning_func( + $options, + $file_name ); - /* Get rid of temporary file */ - unlink( $temp_file_name ); + $file_issues_arr_master = + $tmp_scanning_results['file_issues_arr_master']; - $file_issues_arr_master = json_decode( - $file_issues_str, - true - ); + $file_issues_str = + $tmp_scanning_results['file_issues_str']; + $temp_file_name = + $tmp_scanning_results['temp_file_name']; /* * Do sanity-checking @@ -508,7 +543,7 @@ function( $item ) { gc_collect_cycles(); - vipgoci_runtime_measure( 'stop', 'phpcs_scan_single_file' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'phpcs_scan_single_file' ); } @@ -532,13 +567,6 @@ function( $item ) { foreach ( $prs_implicated as $pr_item ) { - /* - * Loop through each commit, fetching all comments - * made in relation to that commit - */ - - $prs_comments = array(); - /* * Get all commits related to the current * Pull-Request. @@ -550,19 +578,6 @@ function( $item ) { $github_token ); - foreach ( $pr_item_commits as $pr_item_commit_id ) { - vipgoci_github_pr_reviews_comments_get( - $prs_comments, - $repo_owner, - $repo_name, - $pr_item_commit_id, - $pr_item->created_at, - $github_token - ); - - unset( $pr_item_commit_id ); - } - /* * Loop through each file, get a @@ -606,19 +621,14 @@ function( $item ) { * the ones that the are not found * in the blame-log (meaning that * they are due to commits outside of - * the Pull-Request), and remove - * those which have already been submitted. + * the Pull-Request). */ $file_issues_arr_filtered = vipgoci_issues_filter_irrellevant( - $repo_owner, - $repo_name, - $commit_id, $file_name, $files_issues_arr, $file_blame_log, $pr_item_commits, - $prs_comments, $file_relative_lines ); @@ -634,7 +644,7 @@ function( $item ) { $commit_issues_submit[ $pr_item->number ][] = array( - 'type' => 'phpcs', + 'type' => VIPGOCI_STATS_PHPCS, 'file_name' => $file_name, @@ -667,7 +677,6 @@ function( $item ) { } } - unset( $prs_comments ); unset( $pr_item_commits ); unset( $pr_item_files_changed ); unset( $file_blame_log ); @@ -691,6 +700,6 @@ function( $item ) { gc_collect_cycles(); - vipgoci_runtime_measure( 'stop', 'phpcs_scan_commit' ); + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'phpcs_scan_commit' ); } diff --git a/svg-scan.php b/svg-scan.php new file mode 100644 index 000000000..c9ad10017 --- /dev/null +++ b/svg-scan.php @@ -0,0 +1,223 @@ + $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'commit_id' => $options['commit'], + 'svg_checks' => $options['svg-checks'], + 'file_name' => $file_name, + ) + ); + + /* + * These tokens are not allowed + * in SVG files. Note that we do + * a case insensitive search for these. + */ + + $disallowed_tokens = array( + ' $options['repo-owner'], + 'repo_name' => $options['repo-name'], + 'commit_id' => $options['commit'], + 'svg_checks' => $options['svg-checks'], + 'file_name' => $file_name, + ) + ); + + + return null; + } + + $temp_file_name = vipgoci_save_temp_file( + 'svg-scan-', + $file_extension, + $file_contents + ); + + $file_contents = file_get_contents( + $temp_file_name + ); + + unlink( $temp_file_name ); + + /* + * Explode each line into + * each item in an array. + */ + $file_lines_arr = explode( + PHP_EOL, + $file_contents + ); + + /* + * Array for scanning results, + * line counter. + */ + $results_files = array(); + + $line_no = 1; // Line numbers begin at 1 + + /* + * Loop through each line of the + * file, look for disallowed tokens, + * record any found and keep statistics. + */ + foreach ( $file_lines_arr as $file_line_item ) { + /* + * Prepare results array, assume nothing + * is wrong until proven otherwise. + */ + if ( ! isset( $results_files[ $temp_file_name ] ) ) { + $results_files[ $temp_file_name ] = array( + 'errors' => 0, + 'warnings' => 0, + 'fixable' => 0, + 'messages' => array(), + ); + } + + /* + * Scan for each disallowed token + */ + foreach( $disallowed_tokens as $disallowed_token ) { + /* + * Do a case insensitive search + */ + $token_pos = stripos( + $file_line_item, + $disallowed_token + ); + + if ( false === $token_pos ) { + continue; + } + + /* + * Found a problem, adding to results. + */ + + $results_files[ $temp_file_name ]['errors']++; + + $results_files[ $temp_file_name ]['messages'][] = + array( + 'message' => + 'Found forbidden tag in SVG ' . + 'file: \'' . + $disallowed_token . + '\'', + + 'source' => + 'WordPressVIPMinimum.' . + 'Security.SVG.DisallowedTags', + + 'severity' => 5, + 'fixable' => false, + 'type' => 'ERROR', + 'line' => $line_no, + 'column' => $token_pos, + ); + } + + $line_no++; + } + + /* + * Emulate results returned + * by vipgoci_phpcs_scan_single_file(). + */ + + $results = array( + 'totals' => array( + 'errors' => $results_files[ + $temp_file_name + ]['errors'], + + 'warnings' => $results_files[ + $temp_file_name + ]['warnings'], + + 'fixable' => $results_files[ + $temp_file_name + ]['fixable'], + ), + + 'files' => array( + $temp_file_name => + $results_files[ + $temp_file_name + ] + ) + ); + + vipgoci_runtime_measure( VIPGOCI_RUNTIME_STOP, 'svg_scan_single_file' ); + + vipgoci_log( + 'SVG scanning of a single file finished', + array( + 'file_issues_arr_master' => $results, + ) + ); + + return array( + 'file_issues_arr_master' => $results, + 'file_issues_str' => json_encode( $results ), + 'temp_file_name' => $temp_file_name, + ); +} + diff --git a/vip-go-ci.php b/vip-go-ci.php index d043e1108..311d7cedc 100755 --- a/vip-go-ci.php +++ b/vip-go-ci.php @@ -1,12 +1,18 @@ #!/usr/bin/php 0 ) && + ( $irc_params_defined !== 4 ) + ) { + vipgoci_sysexit( + 'Some IRC API parameters defined but not all; all must be defined to be useful', + array( + ), + VIPGOCI_EXIT_USAGE_ERROR + ); + } + + unset( $irc_params_defined ); + + /* + * Handle settings for the pixel API. + */ + if ( isset( $options['pixel-api-url'] ) ) { + vipgoci_option_url_handle( + $options, + 'pixel-api-url', + null + ); + } + + if ( isset( $options['pixel-api-groupname'] ) ) { + $options['pixel-api-groupname'] = trim( + $options['pixel-api-groupname'] + ); + } + + /* * Do some sanity-checking on the parameters + * + * Note: Parameters should not be set after + * this point. */ $options['autoapprove-filetypes'] = array_map( @@ -600,10 +784,7 @@ function vipgoci_run() { if ( ( true === $options['autoapprove'] ) && - ( - ( empty( $options['autoapprove-filetypes'] ) ) || - ( false === $options['autoapprove-label'] ) - ) + ( false === $options['autoapprove-label'] ) ) { vipgoci_sysexit( 'To be able to auto-approve, file-types to approve ' . @@ -614,14 +795,55 @@ function vipgoci_run() { ); } + /* + * Do sanity-checking with hashes-api-url + * and --hashes-oauth-* parameters + */ + if ( isset( $options['hashes-api-url'] ) ) { + foreach ( $hashes_oauth_arguments as $tmp_key ) { + if ( ! isset( $options[ $tmp_key ] ) ) { + vipgoci_sysexit( + 'Asking to use --hashes-api-url without --hashes-oauth-* parameters, but that is not possible, as authorization is needed for hashes-to-hashes API', + array(), + VIPGOCI_EXIT_USAGE_ERROR + ); + } + } + + if ( false === $options['autoapprove'] ) { + vipgoci_sysexit( + 'Asking to use --hashes-api-url without --autoapproval set to true, but for hashes-to-hashes functionality to be useful, --autoapprove must be enabled. Otherwise the functionality will not really be used', + array(), + VIPGOCI_EXIT_USAGE_ERROR + ); + } + } if ( ( true === $options['autoapprove'] ) && - ( in_array( 'php', $options['autoapprove-filetypes'], true ) ) + + /* + * Cross-reference: We disallow autoapproving + * PHP and JS files here, because they chould contain + * contain dangerous code. + */ + ( + ( in_array( + 'php', + $options['autoapprove-filetypes'], + true + ) ) + || + ( in_array( + 'js', + $options['autoapprove-filetypes'], + true + ) ) + ) ) { vipgoci_sysexit( - 'PHP files cannot be auto-approved, as they can' . - 'contain serious problems for execution', + 'PHP and JS files cannot be auto-approved on file-type basis, as they ' . + 'can cause serious problems for execution', array( ), VIPGOCI_EXIT_USAGE_ERROR @@ -638,12 +860,14 @@ function vipgoci_run() { ); if ( + ( false === $current_user_info ) || ( ! isset( $current_user_info->login ) ) || ( empty( $current_user_info->login ) ) ) { vipgoci_sysexit( 'Unable to get information about token-holder user from GitHub', - array(), + array( + ), VIPGOCI_EXIT_GITHUB_PROBLEM ); } @@ -669,6 +893,16 @@ function vipgoci_run() { $options_clean = $options; $options_clean['token'] = '***'; + if ( isset( $options_clean['irc-api-token'] ) ) { + $options_clean['irc-api-token'] = '***'; + } + + foreach( $hashes_oauth_arguments as $hashes_oauth_argument ) { + if ( isset( $options_clean[ $hashes_oauth_argument ] ) ) { + $options_clean[ $hashes_oauth_argument ] = '***'; + } + } + vipgoci_log( 'Starting up...', array( @@ -680,8 +914,9 @@ function vipgoci_run() { 'issues' => array(), 'stats' => array( - 'phpcs' => null, - 'lint' => null, + VIPGOCI_STATS_PHPCS => null, + VIPGOCI_STATS_LINT => null, + VIPGOCI_STATS_HASHES_API => null, ), ); @@ -805,7 +1040,6 @@ function vipgoci_run() { $results ); - /* * Clean up old comments made by us previously */ @@ -827,7 +1061,7 @@ function vipgoci_run() { vipgoci_lint_scan_commit( $options, $results['issues'], - $results['stats']['lint'] + $results['stats'][ VIPGOCI_STATS_LINT ] ); } @@ -840,25 +1074,100 @@ function vipgoci_run() { vipgoci_phpcs_scan_commit( $options, $results['issues'], - $results['stats']['phpcs'] + $results['stats'][ VIPGOCI_STATS_PHPCS ] ); } /* - * If to auto-approve, then do so. + * If to do auto-approvals, then do so now. + * First ask all 'auto-approval modules' + * to do their scanning, collecting all files that + * can be auto-approved, and then actually do the + * auto-approval if possible. */ - if ( true === $options['autoapprove'] ) { - // FIXME: Do not auto-approve if there are - // any linting or PHPCS-issues. + /* + * If to auto-approve based on file-types, + * scan through the files in the PR, and + * register which can be auto-approved. + */ + $auto_approved_files_arr = array(); + + if ( ! empty( $options[ 'autoapprove-filetypes' ] ) ) { + vipgoci_ap_file_types( + $options, + $auto_approved_files_arr + ); + } + + /* + * Do scanning of all altered files, using + * the hashes-to-hashes database API, collecting + * which files can be auto-approved. + */ + + if ( true === $options['hashes-api'] ) { + vipgoci_ap_hashes_api_scan_commit( + $options, + $results['issues'], + $results['stats'][ VIPGOCI_STATS_HASHES_API ], + $auto_approved_files_arr + ); + } + + if ( true === $options['svg-checks'] ) { + vipgoci_ap_svg_files( + $options, + $auto_approved_files_arr + ); + } + vipgoci_auto_approval( - $options + $options, + $auto_approved_files_arr, + $results // FIXME: dry-run ); } /* - * Submit any issues to GitHub + * Remove issues from $results for files + * that are approved in hashes-to-hashes API. + */ + + vipgoci_approved_files_comments_remove( + $options, + $results, + $auto_approved_files_arr + ); + + /* + * Remove comments from $results that have + * already been submitted. + */ + + vipgoci_remove_existing_github_comments_from_results( + $options, + $prs_implicated, + $results, + true + ); + + /* + * Limit number of issues in $results. + * + * If set to zero, skip this part. + */ + + if ( 0 !== $options['review-comments-total-max'] ) { + vipgoci_github_results_filter_comments_to_max( + $options, + $results + ); + } + + /* + * Submit any remaining issues to GitHub */ vipgoci_github_pr_generic_comment_submit( @@ -883,26 +1192,103 @@ function vipgoci_run() { $options['review-comments-max'] ); + if ( true === $options['dismiss-stale-reviews'] ) { + /* + * Dismiss any reviews that contain *only* + * inactive comments -- i.e. comments that + * are obsolete as the code has been changed. + * + * Note that we do this again here because we might + * just have deleted comments from a Pull-Request which + * would then remain without comments. + */ + + foreach ( $prs_implicated as $pr_item ) { + vipgoci_github_pr_reviews_dismiss_non_active_comments( + $options, + $pr_item->number + ); + } + } + + /* + * Send out to IRC API any alerts + * that are queued up. + */ + + if ( + ( ! empty( $options['irc-api-url'] ) ) && + ( ! empty( $options['irc-api-token'] ) ) && + ( ! empty( $options['irc-api-bot'] ) ) && + ( ! empty( $options['irc-api-room'] ) ) + ) { + vipgoci_irc_api_alerts_send( + $options['irc-api-url'], + $options['irc-api-token'], + $options['irc-api-bot'], + $options['irc-api-room'] + ); + } + $github_api_rate_limit_usage = vipgoci_github_rate_limit_usage( $options['token'] ); + /* + * Prepare to send statistics to external service, + * also keep for exit-message. + */ + $counter_report = vipgoci_counter_report( + 'dump', + null, + null + ); + + /* + * Actually send statistics if configured + * to do so. + */ + + if ( + ( ! empty( $options['pixel-api-url'] ) ) && + ( ! empty( $options['pixel-api-groupname' ] ) ) + ) { + vipgoci_send_stats_to_pixel_api( + $options['pixel-api-url'], + $options['pixel-api-groupname'], + + /* + * Which statistics to send. + */ + array( + 'github_pr_approval', + 'github_pr_non_approval', + 'github_api_request_get', + 'github_api_request_post', + 'github_api_request_put', + 'github_api_request_fetch', + 'github_api_request_delete', + ), + $counter_report + ); + } + + + /* + * Final logging before quitting. + */ vipgoci_log( 'Shutting down', array( 'run_time_seconds' => time() - $startup_time, 'run_time_measurements' => vipgoci_runtime_measure( - 'dump', - null - ), - 'counters_report' => - vipgoci_counter_report( - 'dump', - null, + VIPGOCI_RUNTIME_DUMP, null ), + 'counters_report' => $counter_report, + 'github_api_rate_limit' => $github_api_rate_limit_usage->resources->core, @@ -911,6 +1297,9 @@ function vipgoci_run() { ); + /* + * Determine exit code. + */ return vipgoci_exit_status( $results );