Automating Code Plagiarism Detection in GitHub Actions With Codequiry

Automating code plagiarism detection in GitHub Actions is straightforward with Codequiry's REST API. In this guide, you'll build a CI pipeline that submits every student push to Codequiry, receives a similarity score, flags suspicious submissions, and optionally runs AI-generated code detection — all without leaving GitHub. We'll write the workflow YAML, a Python client script, and set thresholds so false positives stay low.

What You'll Build

By the end of this walkthrough, you'll have a GitHub repository that automatically runs a plagiarism check on every push or pull request. The workflow will:

  • Zip the current source code (excluding .git).
  • Submit the archive to Codequiry's API with your API key.
  • Poll for the analysis result, which includes token similarity, AST fingerprint matches, web source matches, and an AI-generated probability.
  • Print a summary to the Actions log and fail the build if similarity exceeds a configurable threshold.

If you're new to Codequiry's code plagiarism checker, create a free account first. The free tier includes a limited number of checks per month, enough to prototype this workflow before rolling it out to a full class or engineering team.

Prerequisites

  • A GitHub account with a repository you control (public or private).
  • A Codequiry API key. You can generate one from the dashboard under Settings → API Keys.
  • Python 3.9 or later installed locally (for testing the script before committing).
  • The requests library: pip install requests.

Step 1 — Create a Codequiry Account and Get an API Key

Go to codequiry.com and sign up. Once logged in, navigate to the API section. You'll see a dashboard showing your account limits and a button to generate a new API key. Copy the key and store it as a GitHub secret:

  1. In your GitHub repository, go to Settings → Secrets and variables → Actions.
  2. Click New repository secret.
  3. Name it CODEQUIRY_API_KEY and paste the key.

This secret will be available to the workflow as ${{ secrets.CODEQUIRY_API_KEY }}, so you never hard-code credentials into your YAML.

Codequiry dashboard home with recent checks showing peer, web and AI similarity scores
The Codequiry dashboard — recent checks at a glance with peer, web and AI similarity scores.

Step 2 — Design the Submission Trigger

Decide when you want the check to run. For a CS course, you likely want to check every push to a student's repository. For an enterprise scenario where you're verifying contractor code, you might only want to check pull requests. The workflow YAML supports both triggers easily:

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]

If you're using GitHub Classroom, student repos are often named with a prefix like assignment1-username. You can set up the same workflow template in the starter repository, so every student inherits the check automatically.

Step 3 — Write the GitHub Actions Workflow

Create a file at .github/workflows/plagiarism-check.yml in your repository. The workflow will install Python, install the requests library, and run a Python script that does the actual checking:

name: Codequiry Plagiarism Check

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]

jobs:
  plagiarism-check:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install requests
      - name: Run Codequiry plagiarism check
        env:
          CODEQUIRY_API_KEY: ${{ secrets.CODEQUIRY_API_KEY }}
          REPO_URL: ${{ github.repository }}
        run: python check_plagiarism.py

This is the entire CI plumbing. The real work happens in check_plagiarism.py, which we'll write next.

Step 4 — The Python Check Script

The script does three things: zip the source, submit it to Codequiry, and poll for the result. Create check_plagiarism.py in the repository root with this content:

import os
import zipfile
import requests
import time
import sys

API_BASE = "https://api.codequiry.com/v1"
API_KEY = os.environ["CODEQUIRY_API_KEY"]
REPO_URL = os.environ.get("REPO_URL", "")

SIMILARITY_THRESHOLD = float(os.environ.get("SIMILARITY_THRESHOLD", "60"))

def zip_source():
    """Create a zip of the current directory excluding .git."""
    with zipfile.ZipFile("submission.zip", "w", zipfile.ZIP_DEFLATED) as zf:
        for root, dirs, files in os.walk("."):
            if ".git" in root:
                dirs[:] = []
                continue
            for file in files:
                zf.write(os.path.join(root, file))
    return "submission.zip"

def submit_check(zip_path):
    """Upload the zip and return the check ID."""
    with open(zip_path, "rb") as f:
        files = {"file": f}
        data = {
            "api_key": API_KEY,
            "repo_url": REPO_URL,
            "ai_detection": "true"
        }
        response = requests.post(f"{API_BASE}/check", files=files, data=data)
        response.raise_for_status()
        return response.json()["check_id"]

def poll_result(check_id):
    """Poll until the check completes, then return the result dict."""
    while True:
        response = requests.get(f"{API_BASE}/check/{check_id}", params={"api_key": API_KEY})
        response.raise_for_status()
        result = response.json()
        if result.get("status") == "complete":
            return result
        time.sleep(5)

if __name__ == "__main__":
    zip_path = zip_source()
    check_id = submit_check(zip_path)
    result = poll_result(check_id)

    similarity = result.get("similarity_score", 0)
    web_matches = result.get("web_matches", [])
    ai_generated = result.get("ai_detection", {}).get("is_ai_generated", False)

    print(f"Overall similarity: {similarity}%")
    print(f"Web source matches: {len(web_matches)}")
    print(f"AI-generated: {ai_generated}")

    if similarity > SIMILARITY_THRESHOLD:
        print(f"EXCEEDS THRESHOLD of {SIMILARITY_THRESHOLD}%")
        sys.exit(1)
    else:
        print("Similarity within acceptable range.")

Test it locally before pushing to GitHub. Run python check_plagiarism.py from your project directory with the API key set as an environment variable. You should see output like:

Overall similarity: 42%
Web source matches: 3
AI-generated: False
Similarity within acceptable range.

The ai_detection=true parameter tells Codequiry to also run its AI detector on the submission. If you only want plagiarism checks, omit that field. But combining both is the most effective way to catch modern academic dishonesty, because students often mix copied snippets with ChatGPT-generated code.

Step 5 — Interpret the Similarity Report

Once the check completes, the result JSON contains more than just an overall percentage. Codequiry's engine performs three types of analysis:

  • Token-based matching — compares normalized token sequences across all submissions in your account's corpus (e.g., all students in the same course).
  • AST fingerprinting — extracts the abstract syntax tree and hashes structural patterns, so renaming variables, reordering methods, or switching loop types doesn't evade detection.
  • Web source matching — searches public GitHub repositories, Stack Overflow posts, and tutorial sites for near-identical code.

The result includes a list of matched snippets with their source file and line numbers. You can click through the dashboard to see a side-by-side comparison. In the CI log, you only get the summary. To get the full report, you can have the script write a markdown file and attach it as an artifact:

with open("report.md", "w") as report:
    report.write(f"# Codequiry Report\n\nCheck ID: {check_id}\n\n")
    report.write(f"Overall similarity: {similarity}%\n\n")
    for match in result.get("matches", [])[:10]:
        report.write(f"- {match['file']} vs {match['matched_file']} ({match['similarity']}%)\n")
    if web_matches:
        report.write("\n## Web matches\n")
        for wm in web_matches[:5]:
            report.write(f"- {wm['source_url']} ({wm['similarity']}%)\n")

Then upload it using actions/upload-artifact@v4 in the workflow.

Codequiry result driller showing a code viewer, match explorer and per-submission analytics
Codequiry's result driller — the matched code, its source, and per-submission analytics on one screen.

Step 6 — Add AI-Generated Code Detection

Code