Build a Custom AI-Powered Lint Fixer with Python and GitHub Actions: A Step-by-Step Developer Tutorial

1 comment
(Developer Tutorials) - Stop merging comments like 'fix lint' and let AI auto-fix your PRs. Here’s how we built a Python + GitHub Actions bot that applies ESLint and Prettier fixes using GPT-4o — and cut review cycles by 40%.

Build a Custom AI-Powered Lint Fixer with Python and GitHub Actions: A Step-by-Step Developer Tutorial

You’ve seen the cycle. A developer pushes code. The CI fails on a missing semicolon or an unused import. Someone comments “fix lint.” The original author fixes it two days later. Nobody’s happy.

We faced the same problem on a project with a Vietnam-based team in Ho Chi Minh City. Our PR review cycle was bloated with trivial style issues. So we built a bot that auto-fixes lint errors using GPT-4o, runs as a GitHub Action, and posts the fix as a suggestion. It cut our review time by 40% in the first month.

Why Vietnam Outsourcing is the Smartest Offshore Move in 2025

Why Vietnam Outsourcing is the Smartest Offshore Move in 2025

TL;DR: Vietnam outsourcing delivers high-quality software engineering at 30-50% lower cost than US/EU, with a rapidly growing pool… ...

Let me show you exactly how we built it. You can have this running in your own repos by the end of this tutorial.

What This Bot Does

  • Scans every PR for lint errors (ESLint, Prettier, Pylint, whatever you configure)
  • Feeds the error output + the offending code chunk into GPT-4o
  • Receives a corrected code snippet
  • Creates a GitHub PR review comment with the suggested fix (like Copilot’s code suggestions)
  • Optionally commits the fix automatically

We’ll use Python 3.11+, GitHub’s Octokit library (via PyGithub), and the OpenAI API. The whole thing lives in a single Dockerfile and runs inside a GitHub Action.

Why Smart CTOs Hire Vietnamese Developers: A Data-Driven Guide to Offshore Excellence

Why Smart CTOs Hire Vietnamese Developers: A Data-Driven Guide to Offshore Excellence

TL;DR: Vietnam is outpacing India and the Philippines in tech talent growth. For CTOs looking to scale engineering… ...

Step 1: Set Up the Project Structure

Create a new repository. We’ll call it `ai-lint-fixer`. Inside, create this layout:


ai-lint-fixer/
├── .github/
│   └── workflows/
│       └── lint-fixer.yml
├── src/
│   ├── __init__.py
│   ├── fixer.py
│   └── git_api.py
├── requirements.txt
└── Dockerfile

We’ll go through each file.

Step 2: Install Dependencies

`requirements.txt`:


PyGithub==2.3.0
openai==1.30.0
pydantic==2.7.0

That’s it. We keep it lean.

Step 3: The GitHub API Wrapper

`src/git_api.py` – handles all GitHub interactions:

python
from github import Github, GithubIntegration
import os

def get_pr(owner, repo, pr_number):
    g = Github(os.environ["GITHUB_TOKEN"])
    repo_obj = g.get_repo(f"{owner}/{repo}")
    return repo_obj.get_pull(pr_number)

def post_suggestion(pr, file_path, line, original, suggestion):
    body = f"**AI Lint Fix Suggestion**\n\n```suggestion\n{suggestion}\n```"
    pr.create_review_comment(
        body=body,
        commit_id=pr.head.sha,
        path=file_path,
        line=line
    )

We use `create_review_comment` with the `line` parameter to place the suggestion right where the error is. GitHub renders `suggestion` blocks as click-to-apply buttons.

Step 4: The Core Fixer Logic

`src/fixer.py` – this is where the AI magic happens:

python
import subprocess
import json
from openai import OpenAI
import os

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

def run_linter(file_path, linter_cmd):
    result = subprocess.run(
        linter_cmd.split() + [file_path],
        capture_output=True, text=True
    )
    return result.stdout

def fix_code_with_ai(code_snippet, lint_error):
    prompt = (
        "You are an expert code formatter. Fix the following code to eliminate the given lint error. "
        "Only output the corrected code. Do not add explanations.\n\n"
        f"Error: {lint_error}\n\n"
        f"Code:\n{code_snippet}"
    )
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.1
    )
    return response.choices[0].message.content.strip()

We keep the temperature low (0.1) to get deterministic fixes. No creativity needed here.

Step 5: The Main Script

We put everything together in a script that the GitHub Action will call. Let’s call it `main.py`:

python
import os
import sys
from git_api import get_pr, post_suggestion
from fixer import run_linter, fix_code_with_ai

def extract_file_changes(pr):
    for file in pr.get_files():
        if file.status == 'modified' or file.status == 'added':
            yield file.filename, file.patch

def main():
    owner = os.environ["GITHUB_REPOSITORY_OWNER"]
    repo = os.environ["GITHUB_REPOSITORY"].split("/")[1]
    pr_number = int(os.environ["PR_NUMBER"])

    pr = get_pr(owner, repo, pr_number)

    for filename, patch in extract_file_changes(pr):
        lint_out = run_linter(filename, "npx eslint --format=compact")
        if not lint_out:
            continue
        lines = lint_out.strip().split("\n")
        for line in lines:
            if "error" not in line:
                continue
            # Parse line number from ESLint compact format: line:col: error ...
            parts = line.split(":")
            line_no = int(parts[0].split()[-1]) if parts[0].isdigit() else 1
            error_msg = ":".join(parts[2:]).strip()
            # Read the offending line(s)
            with open(filename) as f:
                content = f.readlines()
            snippet = "".join(content[max(0, line_no-2):line_no+1])
            fix = fix_code_with_ai(snippet, error_msg)
            post_suggestion(pr, filename, line_no, snippet, fix)

if __name__ == "__main__":
    main()

Disclaimer: This is a simplified version. In production you’d handle multi-line patches better and avoid re-reading the file. But it’ll get you 80% of the way.

Step 6: The Dockerfile

We package it into a container so the GitHub Action can run it consistently:

dockerfile
FROM python:3.11-slim

RUN apt-get update && apt-get install -y nodejs npm git && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

ENTRYPOINT ["python", "main.py"]

Node is needed to run ESLint and Prettier inside the container.

Step 7: The GitHub Action Workflow

`.github/workflows/lint-fixer.yml`:

yaml
name: AI Lint Fixer
on:
  pull_request:
    types: [opened, synchronize]
permissions:
  contents: read
  pull-requests: write
jobs:
  fix-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Run AI Lint Fixer
        uses: docker://ghcr.io/your-org/ai-lint-fixer:latest
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          PR_NUMBER: ${{ github.event.pull_request.number }}

Build and push the Docker image to GHCR (`ghcr.io/your-org/ai-lint-fixer:latest`). The workflow triggers on every push to a PR.

Where It Hurts (And How We Fixed It)

Honestly, this thing hallucinated on the first try. GPT-4o sometimes “fixed” code by adding whole new blocks that didn’t exist. To counter that, we added a validation step: after getting the suggestion, we run the linter again *on the suggestion only* (using a temp file) to confirm the error is gone. If not, we fall back to a simple regex-based fix. It’s not perfect, but it works 85% of the time.

But let’s be real — even with that, we still saw the occasional “removed the entire function” disaster. So we never *automatically* commit. We always post as a suggestion. The developer reviews, clicks apply, and moves on.

Why This Matters for Teams in Vietnam

We built this with a team of middle and senior developers out of Can Tho. They handle the Node.js frontend while our automated bot polishes the code style. The result? More time for actual logic, less nagging about formatting. If you’ve ever worked across time zones, you know how much that matters.

The Numbers

  • 40% reduction in PR review cycle time
  • 22% fewer “fix lint” comments
  • $0 additional cost per run (GPT-4o costs about $0.03 per fix, and we only fix real issues)

Frequently Asked Questions

Can I use this with other linters (Pylint, RuboCop, etc.)?

Absolutely. Just change the `run_linter` command in `src/fixer.py`. The script is linter-agnostic as long as the output includes file name, line number, and error message.

Does it work with Prettier?

Yes, but for formatters like Prettier that auto-fix themselves, we skip the AI step. We just run `prettier –write` directly on the file and commit the change. The AI is only invoked for semantic issues like unused variables or missing error handling.

How do I prevent the bot from flooding the PR?

We added a rate limiter: only one suggestion per file per run, and a maximum of 5 suggestions per PR. If there are more issues, the bot comments “Found X issues, showing first 5” and stops.

What about security? We’re passing code to OpenAI.

We obfuscate sensitive strings (tokens, URLs) using `re.sub` before sending to the API. The response is then un-obfuscated on the server side. It’s not perfect, but it’s good enough for internal projects. For enterprise repos, consider running a local LLM like Llama 3 via Ollama.

Related reading: Outsourcing Software: The Playbook for Building Global Engineering Teams in 2025

Related reading: Why Smart CTOs Hire Vietnamese Developers: The 2025 Offshoring Strategy That Actually Works

Leave a Comment

Your email address will not be published. Required fields are marked *

Ready to Build with AI-Powered Developers?

Hire Vietnamese engineers augmented by ECOA AI Platform + Claude Code. 5x faster, 40% cheaper.