diff --git a/README.md b/README.md index 4059da8..49a34fc 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # Static Analysis -GitHub action for CMake based C++ project, that runs [cppcheck](http://cppcheck.sourceforge.net/) and [clang-tidy](https://clang.llvm.org/extra/clang-tidy/). This action works both no push and on pull requests. +GitHub action for CMake based C++ project, that runs [cppcheck](http://cppcheck.sourceforge.net/) and [clang-tidy](https://clang.llvm.org/extra/clang-tidy/). This action works on both push and pull requests. -In order for this action to work properly, your project has to be CMake based and also include ```.clang-tidy``` file in your root directory. If your project requires some additional packages to be installed, you can use `apt_pckgs` and/or `init_script` input variables to install them (see the **Workflow example** or **Inputs** sections below) +In order for this action to work properly, your project has to be CMake based and it's also recomennded to use ```.clang-tidy``` file, which should be located in your root directory. If your project requires some additional packages to be installed, you can use `apt_pckgs` and/or `init_script` input variables to install them (see the **Workflow example** or **Inputs** sections below). Also, if your repository should allow contribiutions from forks, then it's required to use this Action with `pull_request_target` trigger event, otherwise the GitHub API won't allow to create PR comments. -- **Cppcheck** will run with the following default flags:
+- **cppcheck** will run with the following default flags:
```--enable=all --suppress=missingInclude --inline-suppr --inconclusive``` You can use `cppcheck_args` input to set your flags. -- **clang-tidy** will look for the ```.clang-tidy``` file in your repository. +- **clang-tidy** will look for the ```.clang-tidy``` file in your repository, or you can set checks via `clang_tidy_args` input. ## Pull Request comment @@ -31,8 +31,19 @@ For non Pull Requests, the output will be printed to GitHub's output console. Th ```yml name: Static analysis -# This example runs on per PR basis. This can be changed to also work on push -on: [pull_request] +on: + # Will run on push when merging to 'branches'. The output will be shown in the console + push: + branches: + - develop + - master + - main + + # 'pull_request_target' allows this Action to also run on forked repositories + # The output will be shown in PR comments (unless the 'force_console_print' flag is used) + pull_request_target: + branches: + - "*" jobs: static_analysis: @@ -61,6 +72,12 @@ jobs: # Additional script that will be run (sourced) AFTER 'apt_pckgs' and before running Cmake init_script: init_script.sh + + # (Optional) clang-tidy args + clang_tidy_args: -checks='*,fuchsia-*,google-*,zircon-*,abseil-*,modernize-use-trailing-return-type' + + # (Optional) cppcheck args + cppcheck_args: --enable=all --suppress=missingInclude ``` ## Inputs @@ -74,7 +91,7 @@ jobs: | `apt_pckgs` | FALSE | Additional (space separated) packages that need to be installed in order for project to compile | `` | | `init_script` | FALSE | Optional shell script that will be run before running CMake command. This should be used, when the project requires some environmental set-up beforehand. | `` | | `cppcheck_args` | FALSE | Cppcheck (space separated) arguments that will be used |`--enable=all --suppress=missingInclude --inline-suppr --inconclusive`| -| `clang_tidy_args` | FALSE | clang-tidy (space separated) arguments that will be used |``| +| `clang_tidy_args` | FALSE | clang-tidy arguments that will be used (example: `-checks='*,fuchsia-*,google-*,zircon-*'` |``| | `report_pr_changes_only`| FALSE | Only post the issues found within the changes introduced in this Pull Request. This means that only the issues found within the changed lines will po posted. Any other issues caused by these changes in the repository, won't be reported, so in general you should run static analysis on entire code base |`false`| | `cmake_args` | FALSE | Additional CMake arguments |``| | `force_console_print` | FALSE | Output the action result to console, instead of creating the comment |`false`| diff --git a/action.yml b/action.yml index 887b844..142d8a3 100644 --- a/action.yml +++ b/action.yml @@ -11,6 +11,12 @@ inputs: repo: description: 'Repository name' default: ${{ github.repository }} + pr_repo: + description: 'Head repository (This is useful when using Action with [pull_request_target])' + default: ${{ github.event.pull_request.head.repo.full_name }} + pr_head: + description: 'Head (branch) for PR (same as `pr_repo` input, useful with [pull_request_target]' + default: ${{ github.event.pull_request.head.ref }} comment_title: description: 'Title for comment with the raport. This should be an unique name' default: Static analysis result @@ -24,11 +30,10 @@ inputs: 'This should be used, when the project requires some environmental set-up beforehand' 'Note. `apt_pckgs` will run before this script, just in case you need some packages installed' cppcheck_args: - description: 'Cppcheck (space separated) arguments that will be used' + description: 'cppcheck (space separated) arguments that will be used' default: --enable=all --suppress=missingInclude --inline-suppr --inconclusive clang_tidy_args: - description: 'clang-tidy (space separated) arguments that will be used' - default: "" + description: 'clang-tidy arguments that will be used (example: -checks="*,fuchsia-*,google-*,zircon-*"' report_pr_changes_only: description: 'Only post the issues found within the changes introduced in this Pull Request' default: false diff --git a/entrypoint.sh b/entrypoint.sh index 475e71b..8eb95f4 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,16 +1,28 @@ #!/bin/bash +# shellcheck disable=SC2155 + +set -e export TERM=xterm-color +debug_print() { + if [ "$INPUT_VERBOSE" = "true" ]; then + IFS=$'\n' read -ra ADDR <<< "$1" + for i in "${ADDR[@]}"; do + echo -e "\u001b[32m $i" + done + fi +} + print_to_console=${INPUT_FORCE_CONSOLE_PRINT} if [ $print_to_console = true ]; then echo "The 'force_console_print' option is enabled. Printing output to console." elif [ -z "$INPUT_PR_NUM" ]; then - echo "Pull request number input is not present. Printing output to console." + echo "Pull request number input (pr_num) is not present. Printing output to console." print_to_console=true else - echo "Pull request numer is ${INPUT_PR_NUM}" + debug_print "Pull request number: ${INPUT_PR_NUM}" fi if [ -n "$INPUT_APT_PCKGS" ]; then @@ -23,18 +35,45 @@ if [ -n "$INPUT_INIT_SCRIPT" ]; then source "$INPUT_INIT_SCRIPT" fi +debug_print "Repo = ${INPUT_PR_REPO} SHA = ${INPUT_PR_HEAD} event name = ${GITHUB_EVENT_NAME}" + +use_extra_directorty=false + +# This is useful when running this Action from fork (together with [pull_request_target]) +if [ "$GITHUB_EVENT_NAME" = "pull_request_target" ] && [ -n "$INPUT_PR_REPO" ]; then + debug_print "Running in [pull_request_target] event! Cloning the Head repo ..." + git clone "https://www.github.com/$INPUT_PR_REPO" pr_tree + cd pr_tree || exit + git checkout "$INPUT_PR_HEAD" + use_extra_directorty=true + + # Override commit SHA, in order to get the correct code snippet + NEW_GITHUB_SHA=$(git rev-parse HEAD) + export GITHUB_SHA=$NEW_GITHUB_SHA + + export GITHUB_WORKSPACE=$(pwd) +fi + mkdir build && cd build || exit + cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON "$INPUT_CMAKE_ARGS" .. files_to_check=$(python3 /get_files_to_check.py -exclude="$INPUT_EXCLUDE_DIR" -json="compile_commands.json") +debug_print "Files to check = $files_to_check" +debug_print "INPUT_CPPCHECK_ARGS = $INPUT_CPPCHECK_ARGS" +debug_print "INPUT_CLANG_TIDY_ARGS = $INPUT_CLANG_TIDY_ARGS" + if [ -z "$INPUT_EXCLUDE_DIR" ]; then + debug_print "Running cppcheck --project=compile_commands.json $INPUT_CPPCHECK_ARGS --output-file=cppcheck.txt ..." eval cppcheck --project=compile_commands.json "$INPUT_CPPCHECK_ARGS" --output-file=cppcheck.txt else + debug_print "Running cppcheck --project=compile_commands.json $INPUT_CPPCHECK_ARGS --output-file=cppcheck.txt -i$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR ..." eval cppcheck --project=compile_commands.json "$INPUT_CPPCHECK_ARGS" --output-file=cppcheck.txt -i"$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR" fi # Excludes for clang-tidy are handled in python script +debug_print "Running clang-tidy-12 $INPUT_CLANG_TIDY_ARGS -p $(pwd) $files_to_check -- > clang_tidy.txt" eval clang-tidy-12 "$INPUT_CLANG_TIDY_ARGS" -p "$(pwd)" "$files_to_check" -- >"clang_tidy.txt" -python3 /run_static_analysis.py -cc cppcheck.txt -ct clang_tidy.txt -o $print_to_console +python3 /run_static_analysis.py -cc cppcheck.txt -ct clang_tidy.txt -o $print_to_console -fk $use_extra_directorty diff --git a/run_static_analysis.py b/run_static_analysis.py index 6d783d2..55831e3 100644 --- a/run_static_analysis.py +++ b/run_static_analysis.py @@ -6,12 +6,14 @@ # Input variables from Github action GITHUB_TOKEN = os.getenv("INPUT_GITHUB_TOKEN") PR_NUM = os.getenv("INPUT_PR_NUM") -WORK_DIR = os.getenv("GITHUB_WORKSPACE") +WORK_DIR = f'{os.getenv("GITHUB_WORKSPACE")}' REPO_NAME = os.getenv("INPUT_REPO") +TARGET_REPO_NAME = os.getenv("INPUT_REPO") SHA = os.getenv("GITHUB_SHA") COMMENT_TITLE = os.getenv("INPUT_COMMENT_TITLE") ONLY_PR_CHANGES = os.getenv("INPUT_REPORT_PR_CHANGES_ONLY") VERBOSE = os.getenv("INPUT_VERBOSE", "False").lower() == "true" +FILES_WITH_ISSUES = dict() # Max characters per comment - 65536 # Make some room for HTML tags and error message @@ -23,14 +25,21 @@ CURRENT_COMMENT_LENGTH = 0 +def debug_print(message): + if VERBOSE: + lines = message.split("\n") + for line in lines: + print(f"\033[96m {line}") + + def is_part_of_pr_changes(file_path, issue_file_line, files_changed_in_pr): if ONLY_PR_CHANGES == "false": return True file_name = file_path[file_path.rfind("/") + 1 :] - print(f"Looking for issue found in file={file_name} ...") + debug_print(f"Looking for issue found in file={file_name} ...") for file, (status, lines_changed_for_file) in files_changed_in_pr.items(): - print( + debug_print( f"Changed file by this PR {file} with status {status} and changed lines {lines_changed_for_file}" ) if file == file_name: @@ -82,10 +91,10 @@ def setup_changed_files(): files_changed = dict() github = Github(GITHUB_TOKEN) - repo = github.get_repo(REPO_NAME) + repo = github.get_repo(TARGET_REPO_NAME) pull_request = repo.get_pull(int(PR_NUM)) num_changed_files = pull_request.changed_files - print(f"Changed files {num_changed_files}") + debug_print(f"Changed files {num_changed_files}") files = pull_request.get_files() for file in files: if file.patch is not None: @@ -108,7 +117,9 @@ def is_excluded_dir(line): excluded_dir = f"{WORK_DIR}/{exclude_dir}" if VERBOSE: - print(f"{line} and {excluded_dir} with result {line.startswith(excluded_dir)}") + debug_print( + f"{line} and {excluded_dir} with result {line.startswith(excluded_dir)}" + ) return line.startswith(excluded_dir) @@ -123,6 +134,7 @@ def create_comment_for_output( ): issues_found = 0 global CURRENT_COMMENT_LENGTH + global FILES_WITH_ISSUES output_string = "" for line in tool_output: if line.startswith(prefix) and not is_excluded_dir(line): @@ -140,10 +152,34 @@ def create_comment_for_output( file_line_end = get_file_line_end(file_path, file_line_start) description = f"\n```diff\n!Line: {file_line_start} - {line[line.index(' ')+1:]}``` \n" - new_line = ( - f"\n\nhttps://github.com/{REPO_NAME}/blob/{SHA}{file_path}" - f"#L{file_line_start}-L{file_line_end} {description}
\n" - ) + if TARGET_REPO_NAME != REPO_NAME: + + if file_path not in FILES_WITH_ISSUES: + with open(f"../{file_path}") as file: + lines = file.readlines() + FILES_WITH_ISSUES[file_path] = lines + + modified_content = FILES_WITH_ISSUES[file_path][ + file_line_start - 1 : file_line_end - 1 + ] + modified_content[0] = modified_content[0][:-1] + " <---- HERE\n" + file_content = "".join(modified_content) + + file_url = f"https://github.com/{REPO_NAME}/blob/{SHA}{file_path}#L{file_line_start}" + new_line = ( + "\n\n------" + f"\n\n Issue found in file [{REPO_NAME + file_path}]({file_url})\n" + f"```cpp\n" + f"{file_content}" + f"\n``` \n" + f"{description}
\n" + ) + + else: + new_line = ( + f"\n\nhttps://github.com/{REPO_NAME}/blob/{SHA}{file_path}" + f"#L{file_line_start}-L{file_line_end} {description}
\n" + ) if is_part_of_pr_changes(file_path, file_line_start, files_changed_in_pr): if check_for_char_limit(new_line): @@ -172,6 +208,19 @@ def read_files_and_parse_results(): help="Whether to output the result to console", required=True, ) + parser.add_argument( + "-fk", + "--fork_repository", + help="Whether the actual code is in 'pr_tree' directory", + required=True, + ) + + if parser.parse_args().fork_repository == "true": + global REPO_NAME + + # Make sure to use Head repository + REPO_NAME = os.getenv("INPUT_PR_REPO") + cppcheck_file_name = parser.parse_args().cppcheck clangtidy_file_name = parser.parse_args().clangtidy output_to_console = parser.parse_args().output_to_console == "true" @@ -185,10 +234,12 @@ def read_files_and_parse_results(): clang_tidy_content = file.readlines() line_prefix = f"{WORK_DIR}" - if VERBOSE: - print(f"Cppcheck result: \n {cppcheck_content} \n") - print(f"clang-tidy result: \n {clang_tidy_content} \n") - print(f"line_prefix: {line_prefix} \n") + + debug_print( + f"cppcheck result: \n {cppcheck_content} \n" + f"clang-tidy result: \n {clang_tidy_content} \n" + f"line_prefix: {line_prefix} \n" + ) files_changed_in_pr = dict() if not output_to_console: @@ -236,7 +287,7 @@ def prepare_comment_body( if len(cppcheck_comment) > 0: full_comment_body += ( - f"
:red_circle: Cppcheck found " + f"
:red_circle: cppcheck found " f"{cppcheck_issues_found} {'issues' if cppcheck_issues_found > 1 else 'issue'}!" " Click here to see details.
" f"{cppcheck_comment}
" @@ -255,14 +306,14 @@ def prepare_comment_body( if CURRENT_COMMENT_LENGTH == COMMENT_MAX_SIZE: full_comment_body += f"\n```diff\n{MAX_CHAR_COUNT_REACHED}\n```" - print(f"Repo={REPO_NAME} pr_num={PR_NUM} comment_title={COMMENT_TITLE}") + debug_print(f"Repo={REPO_NAME} pr_num={PR_NUM} comment_title={COMMENT_TITLE}") return full_comment_body def create_or_edit_comment(comment_body): github = Github(GITHUB_TOKEN) - repo = github.get_repo(REPO_NAME) + repo = github.get_repo(TARGET_REPO_NAME) pull_request = repo.get_pull(int(PR_NUM)) comments = pull_request.get_issue_comments()