A DevOps Engineer's Playbook for Plagiarism and Code Smells in CI

Why a Combined Pipeline Matters

Code smells and code plagiarism often co-occur. A student copies a Stack Overflow snippet that introduces a long method; a contractor reuses a closed-source library under GPL. Traditional CI pipelines treat quality and originality as separate concerns. They shouldn't. A single pipeline that runs both static analysis and similarity checks catches more integrity issues earlier, with less context switching.

I've seen teams run SonarQube for quality and then manually check for plagiarism only when a legal complaint arrives. That's reactive. This playbook makes both checks proactive, automated, and visible inside every pull request.

Step 1: Choose Your CI Platform and Repository Structure

The pipeline described here works with GitHub Actions, GitLab CI, or Jenkins. I'll use GitHub Actions for the examples because of its ubiquity and ease of sharing workflows. Your repository should be a monorepo containing multiple services or a single application with a clear entry point. For the guide, assume a Node.js project with a package.json and a src/ directory, but the concepts apply to any language.

# Example repository layout
project/
├── src/
├── tests/
├── package.json
├── sonar-project.properties
└── .github/workflows/integrity.yml

Step 2: Set Up SonarQube for Code Smell Detection

SonarQube is the industry standard for static analysis. It detects code smells, bugs, vulnerabilities, and technical debt. For this pipeline, we'll run a scanner that outputs results as GitHub Checks or a local report.

2.1 Configure SonarQube Server or Cloud

You can use SonarCloud (free for public repos) or self-host SonarQube 9.9+. For a self-hosted instance, I recommend version 10.0 with the latest quality profiles. Create a project key my-org:my-repo and generate an authentication token.

Create a sonar-project.properties file in the project root:

sonar.projectKey=my-org:my-repo
sonar.sources=src
sonar.language=js
sonar.sourceEncoding=UTF-8
sonar.javascript.lcov.reportPaths=coverage/lcov.info

2.2 Add SonarQube Scan Step in GitHub Actions

We'll use the official SonarSource/sonarcloud-github-action. Add the step after building and running tests. The pipeline will fail if the quality gate is not met.

name: Code Integrity Pipeline

on: [pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Needed for blame data in Sonar
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test -- --coverage
      - name: SonarCloud Scan
        uses: SonarSource/sonarcloud-github-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        with:
          args: >
            -Dsonar.qualitygate.wait=true
            -Dsonar.qualitygate.timeout=300

This step blocks the PR if the new code introduces a blocker or critical code smell. Example of a failing rule: "Cognitive Complexity of functions should not be too high" (S3776).

Step 3: Integrate Code Plagiarism Detection with Codequiry

Code smell detection catches how code is written. Code plagiarism detection catches where code came from. Codequiry's API provides token-based similarity checks that work across languages and are resistant to variable renaming and comment changes.

3.1 Obtain Codequiry API Credentials

Sign in to your Codequiry account, navigate to the API Keys page, and generate a new key. Store it as a GitHub secret named CODEQUIRY_API_KEY. You'll also need an account ID, which appears in your dashboard URL.

3.2 Create a Script to Submit Code and Retrieve Results

The Codequiry API accepts a .zip file of the codebase or a list of individual file contents via multipart/form-data. For CI, we'll zip the entire src/ directory and send it for analysis against a pre-configured "class" (a set of known source repositories). Below is a Python script that performs the check and parses the response.

#!/usr/bin/env python3
"""submit_codequiry.py — Send source code for plagiarism analysis."""

import os
import sys
import json
import requests
import zipfile
import io

CODEQUIRY_BASE = os.getenv("CODEQUIRY_BASE_URL", "https://api.codequiry.com/v1")
API_KEY = os.environ.get("CODEQUIRY_API_KEY")
ACCOUNT_ID = os.environ.get("CODEQUIRY_ACCOUNT_ID")
SOURCE_DIR = sys.argv[1] if len(sys.argv) > 1 else "src"

if not API_KEY or not ACCOUNT_ID:
    print("Missing CODEQUIRY_API_KEY or CODEQUIRY_ACCOUNT_ID")
    sys.exit(1)

# Create an in-memory zip of the source directory
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
    for root, _dirs, files in os.walk(SOURCE_DIR):
        for file in files:
            filepath = os.path.join(root, file)
            arcname = os.path.relpath(filepath, start=os.path.dirname(SOURCE_DIR))
            zf.write(filepath, arcname)
zip_buffer.seek(0)

# Submit check
headers = {"Api-Key": API_KEY}
files = {"file": ("source.zip", zip_buffer, "application/zip")}
data = {"account_id": ACCOUNT_ID, "submission_name": "PR-" + os.environ.get("GITHUB_PR_NUMBER", "0")}

resp = requests.post(f"{CODEQUIRY_BASE}/check", headers=headers, files=files, data=data)
if resp.status_code != 200:
    print(f"Submission failed: {resp.text}")
    sys.exit(1)

check_id = resp.json().get("check_id")
print(f"Check submitted: {check_id}")

# Poll for results (Codequiry typically returns in 20-60 seconds)
import time
for _ in range(30):
    time.sleep(5)
    result = requests.get(f"{CODEQUIRY_BASE}/check/{check_id}/result", headers=headers)
    if result.status_code == 200:
        data = result.json()
        if data.get("status") == "completed":
            # Print overall similarity score and top matches
            print(f"Overall similarity: {data['overall_similarity']}%")
            for match in data.get("matches", []):
                print(f"  - {match['source']}: {match['similarity']}% ({match['files_matched']} files)")
            # Fail pipeline if similarity exceeds threshold
            THRESHOLD = float(os.environ.get("PLAGIARISM_THRESHOLD", "30"))
            if data["overall_similarity"] >= THRESHOLD:
                print("FAILURE: Plagiarism threshold exceeded.")
                sys.exit(1)
            else:
                print("PASS: Similarity within acceptable range.")
                sys.exit(0)
        else:
            print(f"Status: {data.get('status')}")
    else:
        print(f"Result fetch failed: {result.text}")
        sys.exit(1)

print("Timeout polling results.")
sys.exit(1)

3.3 Add the Plagiarism Check Step in the Pipeline

Insert this step after the SonarQube scan, as a separate job or inside the same job. The script above expects environment variables CODEQUIRY_API_KEY, CODEQUIRY_ACCOUNT_ID, and GITHUB_PR_NUMBER. The latter is set automatically in GitHub Actions.

...
  plagiarism:
    runs-on: ubuntu-latest
    needs: quality
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install requests
      - name: Run Codequiry plagiarism check
        env:
          CODEQUIRY_API_KEY: ${{ secrets.CODEQUIRY_API_KEY }}
          CODEQUIRY_ACCOUNT_ID: ${{ secrets.CODEQUIRY_ACCOUNT_ID }}
          GITHUB_PR_NUMBER: ${{ github.event.number }}
          PLAGIARISM_THRESHOLD: '30'
        run: python submit_codequiry.py src/

The threshold of 30% is a starting point. For student assignments or contractor audits, I've seen universities set it as low as 15% after tuning. For enterprise internal code, 40% may be appropriate to avoid excessive noise. You'll adjust this after running the pipeline on a few dozen PRs.

Step 4: Combine Results and Block Merges

Both jobs now produce status checks. GitHub's branch protection rules can require both to pass. Navigate to Settings → Branches → Add rule. Under "Require status checks to pass before merging", enable both quality and plagiarism. This ensures no PR with high technical debt or copied code gets merged.

You can also use a third party like Merge Queue to enforce order or run checks in parallel without blocking dependent jobs.

Step 5: Tune Thresholds and Reduce False Positives

No tool is perfect. SonarQube will flag legitimate patterns as code smells (e.g., a well-known algorithm that has high complexity). Codequiry may flag internal code reused across team members. To handle this:

  • SonarQube: Mute irrelevant rules at the project level using the Quality Profile. For JavaScript, I often disable "Function parameters should not have default values" (S1172) in monorepo contexts.
  • Codequiry: Use its "Allowlist" feature to exclude repositories or file patterns that are legitimately shared (e.g., common utility libraries, scaffolding code). Adjust the threshold per branch or per project.
  • Reporting: Send only the top 5 matches to a PR comment via the GitHub API so reviewers can quickly assess.

Here's an example of an automated PR comment from a real run (labels and scores anonymized):

Code Integrity Report
Quality Gate: PASS (0 blocker, 2 critical items)
Plagiarism Check: CAUTION (23% similarity with `legacy-service` repo — likely shared patterns. Review flagged files.)
Details: src/utils/parser.js matches 47% of src/utils/parser.js in legacy-service (identical helper). Consider extracting to a shared package.

Bonus Step: Catch AI-Generated Code in the Pipeline

Codequiry also supports AI-generated code detection. If you suspect contributors are using ChatGPT or Copilot to write large blocks of code, you can add a separate job that calls the /check/ai endpoint. The analysis looks for telltale statistical signatures like low burstiness and uniform token entropy. Add it as a third job with a lower threshold (e.g., 50% AI-likelihood) and flag the PR for manual review.

# In the same pipeline, after the plagiarism check
- name: AI Code Detection
  env:
    CODEQUIRY_API_KEY: ${{ secrets.CODEQUIRY_API_KEY }}
  run: |
    # Example using curl (simplified)
    response=$(curl -s -H "Api-Key: $CODEQUIRY_API_KEY" \
      -F "[email protected]" \
      "https://api.codequiry.com/v1/check/ai")
    ai_score=$(echo "$response" | jq -r '.ai_score')
    if (( $(echo "$ai_score > 50" | bc -l) )); then
      echo "AI-generated code detected (score: $ai_score). Marking for review."
      exit 0  # Soft fail — don't block merge, but flag
    fi

Frequently Asked Questions

How long does a combined scan take on a typical PR?

For a project with 500 source files (Node.js), the full pipeline takes about 3–5 minutes: 2 minutes for build + test + SonarQube, 2 minutes for Codequiry analysis. Parallelizing the two checks can cut wall time in half.

Can I run this on every push instead of only PRs?

Yes, but I recommend PR-only for the plagiarism check to avoid excessive API calls. Codequiry pricing is per submission, so limit to meaningful events. Run SonarQube on every push if you want early feedback, then skip the plagiarism check if no files changed.

What languages does the coverage work for?

SonarQube supports 30+ languages. Codequiry supports Java, Python, JavaScript, C++, C#, Ruby, Go, PHP, Swift, and more. The API treats all code as plain text for token-based matching, so even less common languages work if they share syntax with supported ones.

How do I handle false positives from boilerplate code?

Add boilerplate directories to Codequiry’s exclusion list (e.g., node_modules, vendor, generated stubs). For SonarQube, use sonar.exclusions=**/node_modules/** in the properties file. Both tools support file-level ignore patterns.

Real-World Impact

I set up this pipeline for a university CS department’s capstone projects. In the first semester, 12% of PRs failed the plagiarism check (average similarity > 30%). Half of those turned out to be legitimate shared framework code—the other half were cases where students copied from previous years. The pipeline caught them before graders had to inspect manually. For enterprise teams, similar setups have reduced legal exposure and improved code maintainability by enforcing both quality gates and originality checks together.

The key is not to treat SonarQube and Codequiry as competing tools. They solve different problems at different layers. One answers "Is this code any good?" and the other answers "Is this code ours?" Run them together, tune the thresholds based on real data, and your CI pipeline becomes a genuine integrity guard, not just a test runner.