diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44da2839..51378c97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - # TODO: This is a 3rd party GitHub action from some dude. Ideally, we'd - # use something more "official". + # TODO: This is a 3rd party GitHub action from "some dude". + # Ideally, we'd use something more official. - name: Check if Dockerfile changed uses: dorny/paths-filter@v2 id: filter @@ -187,10 +187,29 @@ jobs: run: | docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/run_cppcheck.sh ghcr.io/blues/note_c_ci:latest + run_scancode: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [build_ci_docker_image] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Load CI Docker image + # Only load the Docker image artifact if build_ci_docker_image actually + # ran (e.g. it wasn't skipped and was successful). + if: ${{ needs.build_ci_docker_image.result == 'success' }} + uses: ./.github/actions/load-ci-image + + - name: Run license compliance check + run: | + docker run --rm --volume $(pwd):/note-c/ --workdir /note-c/ --entrypoint ./scripts/run_scancode.sh ghcr.io/blues/note_c_ci:latest + publish_ci_image: runs-on: ubuntu-latest # Make sure unit tests unit tests passed before publishing. - needs: [build_ci_docker_image, run_unit_tests] + needs: [build_ci_docker_image, run_unit_tests, run_scancode] # Only publish the image if this is a push event and the Docker image was rebuilt if: ${{ github.event_name == 'push' && needs.build_ci_docker_image.result == 'success' }} diff --git a/.gitignore b/.gitignore index d6056bf6..6df71cae 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ settings.json # Development Artifacts cppcheck_output.txt +scancode_console_output.txt diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b00c48ce..ae7d69a1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -55,12 +55,12 @@ } }, { - "label": "Note-C: Run `astyle` Formatter", + "label": "Note-C: Generate Coverage HTML", "type": "shell", - "command": "./scripts/run_astyle.sh", + "command": "genhtml lcov.info -o tmp", "args": [], "options": { - "cwd": "${workspaceFolder}", + "cwd": "${workspaceFolder}/build/test/coverage", "env": { "LC_ALL": "C" } @@ -68,15 +68,18 @@ "problemMatcher": [], "group": { "kind": "none" - } + }, + "dependsOn": [ + "Note-C: Compile and Run ALL Tests (with coverage)" + ] }, { - "label": "Note-C: Generate Coverage HTML", + "label": "Note-C: Generate Documentation", "type": "shell", - "command": "genhtml lcov.info -o tmp", + "command": "./scripts/build_docs.sh", "args": [], "options": { - "cwd": "${workspaceFolder}/build/test/coverage", + "cwd": "${workspaceFolder}", "env": { "LC_ALL": "C" } @@ -84,15 +87,12 @@ "problemMatcher": [], "group": { "kind": "none" - }, - "dependsOn": [ - "Note-C: Compile and Run ALL Tests (with coverage)" - ] + } }, { - "label": "Note-C: Generate Documentation", + "label": "Note-C: Run `astyle` Formatter", "type": "shell", - "command": "./scripts/build_docs.sh", + "command": "./scripts/run_astyle.sh", "args": [], "options": { "cwd": "${workspaceFolder}", @@ -105,6 +105,19 @@ "kind": "none" } }, + { + "label": "Note-C: Run License Compliance Check", + "type": "shell", + "command": "${workspaceFolder}/scripts/run_scancode.sh", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "LC_ALL": "C" + } + }, + "problemMatcher": [], + "group": "test" + }, { "label": "Note-C: Run Static Analysis", "type": "shell", diff --git a/Dockerfile b/Dockerfile index 334df54f..d2e9850e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,6 +69,7 @@ RUN ["dash", "-c", "\ valgrind \ && pip install --break-system-packages \ breathe \ + scancode-toolkit \ sphinx-rtd-theme \ && apt-get clean \ && apt-get purge \ diff --git a/scripts/run_scancode.sh b/scripts/run_scancode.sh new file mode 100755 index 00000000..b8210aaa --- /dev/null +++ b/scripts/run_scancode.sh @@ -0,0 +1,105 @@ +#!/bin/bash +set -exo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +SRC_DIR="$SCRIPT_DIR/.." + +echo "Running License Compliance Analysis..." +echo + +# Create a function to generate the summary +generate_summary() { + { + # Initialize flag + has_critical_issues=false + + echo + + # Always generate and display summary regardless of exit code + echo "=== License Compliance Summary ===" + echo + + # Display license information + echo "License Information:" + echo "-------------------" + jq -r '.summary.declared_license_expression' scancode_output.json || echo "None found" + echo + + # Display license clarity score + echo "License Clarity Score:" + echo "---------------------" + jq -r '.summary.license_clarity_score.score' scancode_output.json || echo "Not available" + echo + + # Display other licenses found + echo "Other Licenses Found:" + echo "--------------------" + jq -r '.summary.other_license_expressions[] | select(.value != null) | "\(.value): \(.count) occurrences"' scancode_output.json || echo "None found" + echo + + # Display copyright holders + echo "Copyright Holders:" + echo "-----------------" + jq -r '.tallies.holders[] | select(.value != null) | "\(.value): \(.count) occurrences"' scancode_output.json || echo "None found" + echo + + # Check for non-compliant licenses + echo "License Compliance Issues:" + echo "-------------------------" + # List of non-compliant or problematic licenses + problematic_licenses=("gpl-2.0" "gpl-3.0" "agpl-3.0" "cc-by-nc" "proprietary") + + found_problematic=false + for license in "${problematic_licenses[@]}"; do + count=$(jq -r ".summary.other_license_expressions[] | select(.value == \"$license\") | .count" scancode_output.json 2>/dev/null || echo "0") + if [ "$count" != "0" ] && [ "$count" != "" ]; then + echo "WARNING: Found $count occurrences of $license license, which may have compliance implications." + found_problematic=true + has_critical_issues=true + fi + done + + if ! $found_problematic; then + echo "No problematic licenses found." + fi + echo + + # Display status and details + if $has_critical_issues; then + echo "Status: FAILED - License compliance issues found" + echo + echo "Review and fix license compliance issues before proceeding" + else + echo "Status: PASSED - No license compliance issues found" + echo + echo "Note: Review license information for potential improvements" + fi + } + + # Return 1 if critical issues found, 0 otherwise + if $has_critical_issues; then + return 1 + else + return 0 + fi +} + +# Run scancode and capture output and exit code +scancode \ + --license \ + --copyright \ + --classify \ + --summary \ + --license-clarity-score \ + --tallies \ + --json-pp scancode_output.json \ + --timeout 120 \ + --processes 4 \ + --ignore "build/*" \ + --verbose \ + $SRC_DIR 2>&1 | tee scancode_console_output.txt + +generate_summary + +# Exit with scancode's status code +exit $?