Use Case

AI Code Review in GitHub Actions

LearnCodeGuide reviews every pull request automatically. Add a workflow file and an API key secret once, and from then on each PR gets bug, security, and quality findings posted inline — no copy-pasting, no manual runs. Every account gets 5 free PR reviews to start; after that, the GitHub Actions plan ($12/month) gives you 30 reviews per month.


1

Copy your API key

Your API key authenticates the action with your LearnCodeGuide account. Every account has one — find it on your dashboard under GitHub Actions setup. Keep it secret — treat it like a password.

2

Add the repository secret

In your repository, go to Settings → Secrets and variables → Actions, click New repository secret, name it LCG_API_KEY, and paste your key as the value. Storing it as a secret keeps it encrypted and out of your code.

3

Add the workflow file

Create .github/workflows/learncodeguide.yml in your repository and paste:

name: LearnCodeGuide Review

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    env:
      LCG_API_KEY: ${{ secrets.LCG_API_KEY }}
    steps:
      - name: Check API key
        id: precheck
        shell: bash
        run: |
          if [ -z "$LCG_API_KEY" ]; then
            echo "::warning::LCG_API_KEY secret is not configured — skipping LearnCodeGuide review."
            echo "skip=true" >> "$GITHUB_OUTPUT"
          else
            echo "skip=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Analyze changed files
        if: steps.precheck.outputs.skip != 'true'
        uses: actions/github-script@v7
        env:
          LCG_API_KEY: ${{ secrets.LCG_API_KEY }}
        with:
          script: |
            const LANG_MAP = {
              '.js':   'javascript',
              '.jsx':  'javascript',
              '.ts':   'typescript',
              '.tsx':  'typescript',
              '.py':   'python',
              '.java': 'java',
              '.cs':   'csharp',
              '.cpp':  'cpp',
            };
            const API_URL = 'https://learncodeguide-app-production.up.railway.app/api/github/analyze';
            const COMMENT_MARKER = '<!-- learncodeguide-review -->';
            const MAX_BODY = 60000;

            const { owner, repo } = context.repo;
            const pr = context.payload.pull_request;
            if (!pr) {
              core.setFailed('No pull_request payload found.');
              return;
            }
            const prNumber = pr.number;
            const headSha  = pr.head.sha;

            const extOf = (name) => {
              const i = name.lastIndexOf('.');
              return i >= 0 ? name.slice(i).toLowerCase() : '';
            };

            // 1) List PR files
            const files = await github.paginate(github.rest.pulls.listFiles, {
              owner, repo, pull_number: prNumber, per_page: 100,
            });

            const targets = files.filter(f => {
              if (f.status === 'removed') return false;
              return LANG_MAP[extOf(f.filename)] != null;
            });

            if (targets.length === 0) {
              core.info('No files with supported extensions changed — nothing to analyze.');
              return;
            }
            core.info(`Analyzing ${targets.length} file(s).`);

            // 2) Fetch content + POST to LCG for each file
            const perFile = [];
            let freeLimit = null;
            for (const f of targets) {
              const language = LANG_MAP[extOf(f.filename)];
              let code = null;
              try {
                const res = await github.rest.repos.getContent({
                  owner, repo, path: f.filename, ref: headSha,
                });
                if (Array.isArray(res.data) || res.data.type !== 'file' || !res.data.content) {
                  core.warning(`Skipping ${f.filename}: not a readable file (size too large or non-file).`);
                  perFile.push({ filename: f.filename, language, error: 'content unavailable' });
                  continue;
                }
                code = Buffer.from(res.data.content, res.data.encoding || 'base64').toString('utf8');
              } catch (err) {
                core.warning(`Failed to read ${f.filename}: ${err.message}`);
                perFile.push({ filename: f.filename, language, error: `read failed: ${err.message}` });
                continue;
              }

              try {
                const resp = await fetch(API_URL, {
                  method: 'POST',
                  headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${process.env.LCG_API_KEY}`,
                  },
                  body: JSON.stringify({ code, language, mode: 'all', doubleCheck: true, repo: `${owner}/${repo}`, prNumber }),
                });
                if (resp.status === 402) {
                  try { freeLimit = await resp.json(); } catch { freeLimit = { message: "You've used your free PR reviews." }; }
                  break;
                }
                if (!resp.ok) {
                  const text = await resp.text();
                  core.warning(`LCG ${resp.status} for ${f.filename}: ${text.slice(0, 300)}`);
                  perFile.push({ filename: f.filename, language, error: `HTTP ${resp.status}` });
                  continue;
                }
                const data = await resp.json();
                if (!data || data.ok !== true) {
                  core.warning(`LCG ok=false for ${f.filename}: ${data && data.error || 'unknown'}`);
                  perFile.push({ filename: f.filename, language, error: (data && data.error) || 'unknown' });
                  continue;
                }
                perFile.push({ filename: f.filename, language, output: data.output || {} });
              } catch (err) {
                core.warning(`Network error analyzing ${f.filename}: ${err.message}`);
                perFile.push({ filename: f.filename, language, error: err.message });
              }
            }

            // Free-tier limit reached -> post a single upgrade notice and stop (don't fail the job).
            if (freeLimit) {
              const upgradeUrl = freeLimit.upgradeUrl || 'https://learncodeguide.com/billing';
              const msg = freeLimit.message || "You've used your free PR reviews.";
              let body = `${COMMENT_MARKER}\n## 🤖 LearnCodeGuide Review\n\n`;
              body += `${msg}\n\n`;
              body += `👉 [Upgrade to the GitHub Actions plan](${upgradeUrl}) to keep getting automatic reviews on every pull request.\n\n`;
              body += `---\n_Powered by [LearnCodeGuide](https://learncodeguide.com)_\n`;
              const existing = await github.paginate(github.rest.issues.listComments, {
                owner, repo, issue_number: prNumber, per_page: 100,
              });
              const prev = existing.find(c => typeof c.body === 'string' && c.body.includes(COMMENT_MARKER));
              if (prev) {
                await github.rest.issues.updateComment({ owner, repo, comment_id: prev.id, body });
              } else {
                await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
              }
              core.info('Free review limit reached — posted upgrade notice.');
              return;
            }

            // 3) Aggregate
            const scores = perFile
              .map(r => r.output && typeof r.output.healthScore === 'number' ? r.output.healthScore : null)
              .filter(v => v !== null);
            const overall = scores.length
              ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
              : null;

            const allIssues = [];
            for (const r of perFile) {
              const issues = (r.output && (r.output.issues || r.output.findings)) || [];
              for (const it of issues) allIssues.push({ ...it, _file: r.filename });
            }
            const isCritical = (it) => String(it.severity || '').toUpperCase() === 'CRITICAL';
            const criticalCount = allIssues.filter(isCritical).length;
            const verdict = criticalCount > 0 ? 'FAIL ❌' : 'PASS ✅';

            // 4) Executive summary — prefer API-provided, otherwise derive 3 bullets
            let execBullets = [];
            for (const r of perFile) {
              const s = r.output && r.output.executiveSummary;
              if (Array.isArray(s) && s.length) { execBullets = s.slice(0, 3); break; }
            }
            if (execBullets.length === 0) {
              execBullets.push(
                overall !== null
                  ? `Overall health score across ${perFile.length} file(s): ${overall}/100.`
                  : `Reviewed ${perFile.length} file(s).`
              );
              execBullets.push(`Total findings: ${allIssues.length} (${criticalCount} critical).`);
              execBullets.push(
                criticalCount > 0
                  ? 'Critical issues require attention before merge.'
                  : 'No blocking issues detected — safe to review.'
              );
            }

            // 5) Build comment
            const sevIcon = (s) => {
              const v = String(s || '').toUpperCase();
              if (v === 'CRITICAL') return '🔴';
              if (v === 'HIGH')     return '🟠';
              if (v === 'MEDIUM')   return '🟡';
              if (v === 'LOW')      return '🟢';
              return '⚪';
            };
            const fence = (lang, body) => '```' + lang + '\n' + String(body) + '\n```';

            let body = `${COMMENT_MARKER}\n## 🤖 LearnCodeGuide Review\n\n`;
            body += `**Health Score (overall):** ${overall !== null ? overall + '/100' : 'n/a'}\n`;
            body += `**Verdict:** ${verdict}\n\n`;
            body += `### Executive Summary\n`;
            for (const b of execBullets.slice(0, 3)) body += `- ${b}\n`;
            body += `\n<details><summary><strong>Detailed findings per file</strong></summary>\n\n`;

            for (const r of perFile) {
              body += `#### \`${r.filename}\` _(${r.language})_\n`;
              if (r.error) {
                body += `> ⚠️ Analysis error: ${r.error}\n\n`;
                continue;
              }
              if (r.output && typeof r.output.healthScore === 'number') {
                body += `Health Score: **${r.output.healthScore}/100**\n\n`;
              }
              const issues = (r.output && (r.output.issues || r.output.findings)) || [];
              if (issues.length === 0) {
                body += `_No issues found._\n\n`;
                continue;
              }
              for (const it of issues) {
                const sev = String(it.severity || 'INFO').toUpperCase();
                const title = it.title || it.message || 'Issue';
                const where = it.line ? ` (line ${it.line})` : '';
                body += `- ${sevIcon(sev)} **${sev}**${where} — ${title}\n`;
                if (it.description) body += `  ${it.description}\n`;
                const fix = it.fix || it.suggestion;
                if (fix) {
                  body += `\n  <details><summary>Suggested fix</summary>\n\n`;
                  body += fence(r.language, fix).split('\n').map(l => '  ' + l).join('\n') + '\n\n';
                  body += `  </details>\n`;
                }
              }
              body += `\n`;
            }
            body += `</details>\n\n---\n_Powered by [LearnCodeGuide](https://learncodeguide.com)_\n`;

            if (body.length > MAX_BODY) {
              body = body.slice(0, MAX_BODY) + '\n\n_…comment truncated to fit GitHub size limit._\n';
            }

            // 6) Upsert single PR comment (marker-based)
            const existing = await github.paginate(github.rest.issues.listComments, {
              owner, repo, issue_number: prNumber, per_page: 100,
            });
            const prev = existing.find(c => typeof c.body === 'string' && c.body.includes(COMMENT_MARKER));
            if (prev) {
              await github.rest.issues.updateComment({ owner, repo, comment_id: prev.id, body });
              core.info(`Updated existing review comment #${prev.id}.`);
            } else {
              await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
              core.info('Posted new review comment.');
            }

            // 7) Fail job if any CRITICAL issue
            if (criticalCount > 0) {
              core.setFailed(`LearnCodeGuide found ${criticalCount} CRITICAL issue(s).`);
            }

Commit the file. On the next pull request, the action checks out your code, sends the diff for analysis, and posts findings and suggested fixes directly on the PR.


Automate your PR reviews

Start with 5 free PR reviews — no card required. Then $12/month for 30 reviews per month.

Get started →

Related Guides

What is LearnCodeGuide?What is Double Check?How to Review Code FasterCode Review Checklist

Published by LearnCodeGuide Team · Last reviewed: June 2026