name: Linter on: [push, pull_request] jobs: lint: # it's easiest if it matches the version that was used to build colobot-lint, newer versions don't have llvm-3.6-dev in repo... runs-on: ubuntu-16.04 env: CC: /usr/lib/llvm-3.6/bin/clang CXX: /usr/lib/llvm-3.6/bin/clang++ CLANG_PREFIX: /usr/lib/llvm-3.6 steps: - name: Download Colobot dependencies run: sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential cmake libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsndfile1-dev libvorbis-dev libogg-dev libpng-dev libglew-dev libopenal-dev libboost-dev libboost-system-dev libboost-filesystem-dev libboost-regex-dev libphysfs-dev gettext git po4a vorbis-tools librsvg2-bin xmlstarlet - name: Download colobot-lint dependencies run: sudo apt-get install -y --no-install-recommends clang-3.6 libtinyxml2.6.2v5 - run: pip install requests - run: mkdir -p /tmp/colobot-lint - name: Download colobot-lint working-directory: /tmp/colobot-lint shell: python env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO_NAME: colobot/colobot-lint BRANCH_NAME: master ARTIFACT_NAME: colobot-lint run: | import os import requests # How there can be no builtin action to download the latest artifact from another repo?! s = requests.Session() s.headers.update({ 'Authorization': 'token ' + os.environ['GITHUB_TOKEN'], 'Accept': 'application/vnd.github.v3+json' }) r = s.get("https://api.github.com/repos/" + os.environ['REPO_NAME'] + "/actions/runs", params={'branch': os.environ['BRANCH_NAME'], 'event': 'push', 'status': 'success'}) r.raise_for_status() # Querying for "dev" returns all branches that have "dev" anywhere in the name... is that a GitHub bug or intended behaviour? runs = list(filter(lambda x: x['head_branch'] == os.environ['BRANCH_NAME'], r.json()['workflow_runs'])) if len(runs) == 0: raise Exception('No valid run found') run = runs[0] print("Using colobot-lint from run #{} ({}) for commit {}".format(run['run_number'], run['id'], run['head_sha'])) r = s.get(run['artifacts_url']) r.raise_for_status() artifacts = list(filter(lambda x: x['name'] == os.environ['ARTIFACT_NAME'], r.json()['artifacts'])) if len(artifacts) != 1: raise Exception('Artifact not found') artifact = artifacts[0] print(artifact['archive_download_url']) r = s.get(artifact['archive_download_url'], stream=True) r.raise_for_status() with open(os.environ['ARTIFACT_NAME'] + '.zip', 'wb') as f: for block in r.iter_content(1024): f.write(block) print("Download finished") - name: Unpack colobot-lint working-directory: /tmp/colobot-lint run: | # Unzip the archive mkdir archive; cd archive unzip ../colobot-lint.zip cd .. # Workaround for Clang not finding system headers mkdir ./bin mv ./archive/build/colobot-lint ./bin/ chmod +x ./bin/colobot-lint ln -s ${CLANG_PREFIX}/lib ./lib # Unpack HtmlReport tar -zxf ./archive/build/html_report.tar.gz # Clean up rm -r ./archive - uses: actions/checkout@v2 - name: Create build directory run: cmake -E make_directory build - name: Run CMake working-directory: build run: cmake -DCOLOBOT_LINT_BUILD=1 -DTESTS=1 -DTOOLS=1 -DCMAKE_EXPORT_COMPILE_COMMANDS=1 .. - name: Run linter shell: bash run: | set -e +x WORKSPACE="$GITHUB_WORKSPACE" COLOBOT_DIR="$WORKSPACE" COLOBOT_BUILD_DIR="$WORKSPACE/build" COLOBOT_LINT_REPORT_FILE="$WORKSPACE/build/colobot_lint_report.xml" cd "/tmp/colobot-lint" find "$WORKSPACE" \( -wholename "$COLOBOT_DIR/src/*.cpp" \ -or -wholename "$COLOBOT_DIR/test/unit/*.cpp" \ -or -wholename "$COLOBOT_BUILD_DIR/fake_header_sources/src/*.cpp" \ -or -wholename "$COLOBOT_BUILD_DIR/fake_header_sources/test/unit/*.cpp" \) \ -exec ./bin/colobot-lint \ -verbose \ -output-format xml \ -output-file "$COLOBOT_LINT_REPORT_FILE" \ -p "$COLOBOT_BUILD_DIR" \ -project-local-include-path "$COLOBOT_DIR/src" -project-local-include-path "$COLOBOT_BUILD_DIR/src" \ -license-template-file "$COLOBOT_DIR/LICENSE-HEADER.txt" \ {} + - name: Upload results (XML) uses: actions/upload-artifact@v2 with: name: XML results path: build/colobot_lint_report.xml - name: Generate HTML report shell: bash run: /tmp/colobot-lint/HtmlReport/generate.py --xml-report "build/colobot_lint_report.xml" --output-dir "build/html_report" - name: Upload results (HTML) uses: actions/upload-artifact@v2 with: name: HTML results path: build/html_report - name: Send linter results to GitHub shell: python env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ACTUALLY_SEND: ${{ github.event.type != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} run: | import os import sys import requests import xml.etree.ElementTree as ET OVERALL_STABLE_RULES=[ "class naming", "code block placement", "compile error", # "compile warning", # "enum naming", # "function naming", "header file not self-contained", # "implicit bool cast", # "include style", # "inconsistent declaration parameter name", "license header", # "naked delete", # "naked new", # "old style function", "old-style null pointer", # "possible forward declaration", "undefined function", # "uninitialized field", # "uninitialized local variable", # "unused forward declaration", # "variable naming", "whitespace", ] STABLE_RULES_WITHOUT_CBOT=[ "class naming", "code block placement", "compile error", "compile warning", # "enum naming", # "function naming", "header file not self-contained", # "implicit bool cast", "include style", "inconsistent declaration parameter name", "license header", "naked delete", "naked new", # "old style function", "old-style null pointer", # "possible forward declaration", "undefined function", "uninitialized field", # "uninitialized local variable", "unused forward declaration", # "variable naming", "whitespace", ] # None of the available actions seem to do what I want, they all do stupid things like adding another check... let's just do it manually # GitHub also doesn't seem to provide you with the check suite or check run ID, so we have to get it from the action ID via the API s = requests.Session() s.headers.update({ 'Authorization': 'token ' + os.environ['GITHUB_TOKEN'], 'Accept': 'application/vnd.github.antiope-preview+json' # Annotations are still technically a preview feature of the API }) action_run = s.get(os.environ['GITHUB_API_URL'] + "/repos/" + os.environ['GITHUB_REPOSITORY'] + "/actions/runs/" + os.environ['GITHUB_RUN_ID']).json() check_suite = s.get(action_run['check_suite_url']).json() check_suite_runs = s.get(check_suite['check_runs_url']).json() check_run = check_suite_runs['check_runs'][0] # NOTE: This assumes that the 'lint' job is the first one in the workflow. You could find it by name if you really wanted. def we_care_about(file_name, type): if 'CBot' in file_name: return type in OVERALL_STABLE_RULES else: return type in STABLE_RULES_WITHOUT_CBOT results = ET.parse('build/colobot_lint_report.xml') annotations = [] for error in results.find('errors').findall('error'): location = error.find('location') file_name = os.path.relpath(location.get('file'), os.environ['GITHUB_WORKSPACE']) line_num = int(location.get('line')) type = error.get('id') severity = error.get('severity') msg = error.get('msg') gh_severity = 'warning' if severity == 'error': gh_severity = 'failure' elif severity == 'information': gh_severity = 'notice' if not we_care_about(file_name, type): # don't send the unstable rules to github at all as there are way too many of them and it would overload the API rate limit continue print('{}:{}: [{}] {}'.format(file_name, line_num, type, msg)) annotations.append({ 'path': file_name, 'start_line': line_num, 'end_line': line_num, 'annotation_level': gh_severity, 'title': type, 'message': msg }) summary = 'colobot-lint found {} issues'.format(len(annotations)) all_ok = len(annotations) == 0 print('Conclusion: {}'.format(summary)) if os.environ['ACTUALLY_SEND'] != "true": print('Skip uploading the results as annotations because tokens from forks are readonly and there seems to be no way to do it. Blame GitHub Actions devs.') else: # Annotations have to be sent in batches of 50 first = True while first or len(annotations) > 0: first = False to_send = annotations[:50] annotations = annotations[50:] data = { 'output': { 'title': summary, 'summary': summary, 'annotations': to_send } } r = s.patch(check_run['url'], json=data) r.raise_for_status() sys.exit(0 if all_ok else 1)