Code review remains one of the most persistent bottlenecks in software development. Pull requests sit idle for hours or days while reviewers context-switch, miss subtle bugs, and apply style feedback inconsistently. Automating code review with DeepSeek in GitHub Actions offers a practical way to address these pain points without abandoning human oversight.
This tutorial walks through building a fully automated PR review pipeline for a JavaScript/React/Node.js project. By the end, every pull request opened against the repository will receive inline AI-generated review comments within seconds, running alongside existing CI/CD jobs and requiring no manual intervention.
Table of Contents
Prerequisites and What You’ll Need
Tools and Accounts
To follow along, you need a GitHub account with access to a repository (public or private), a DeepSeek API key obtained from the official DeepSeek developer portal (verify the current URL at deepseek.com, as portal URLs are subject to change), and a Node.js 18+ project. The example uses a React and Node.js stack, but the workflow generalizes to any JavaScript or TypeScript codebase. Readers should have basic familiarity with GitHub Actions YAML syntax, specifically how workflows, jobs, and steps are structured. Self-hosted DeepSeek deployments require an additional configuration parameter to override the API base URL; consult the action’s documentation for the relevant input name.
Repository Setup
Readers can follow along using their own JS/React project or clone the example repository. No special directory structure is required beyond the standard .github/workflows/ path where the workflow file will live.
How the DeepSeek Code Review Action Works
Architecture Overview
The pipeline follows a straightforward event-driven architecture. GitHub Actions triggers on pull_request events, specifically when a PR is opened or updated with new commits. The DeepSeek code review action (deepseek-ai/code-review-action) extracts the PR diff, constructs a prompt tuned for code review, and sends it to DeepSeek’s API or any compatible endpoint. DeepSeek processes the diff, identifies issues, and returns structured review comments. The action then posts those comments directly on the pull request as an inline review, appearing in the “Files changed” tab just as a human reviewer’s comments would.
Important: Verify the action reference
deepseek-ai/code-review-action@against the DeepSeek GitHub organization (github.com/deepseek-ai) or the GitHub Marketplace before use. If the action is not published, an alternative approach using a customrun:step with the DeepSeek API directly is required. Additionally, verify the accepted input parameters against the action’saction.ymlfile atgithub.com/deepseek-ai/code-review-action/blob/main/action.ymlbefore using the configuration shown in this tutorial.
Key Configuration Options
Several parameters control the review behavior. Model selection determines quality and cost. Verify current model identifiers in the DeepSeek API documentation at platform.deepseek.com/docs before use, as model names change across API versions. deepseek-coder targets code understanding, while deepseek-chat provides broader conversational reasoning. You can limit review scope to diff hunks or expand it to include the full content of changed files for additional context. Use diff-only for PRs under roughly 500 lines; use full-file scope for smaller, context-heavy changes where surrounding code matters. Comment verbosity and severity filtering are controlled through the review prompt itself (see Step 3 for examples of how to instruct DeepSeek to categorize and suppress low-priority notes). Token and cost management settings cap response length to prevent unexpectedly large API bills.
Step 1: Storing Your DeepSeek API Key as a GitHub Secret
Navigate to the repository’s Settings page, then to Secrets and variables, and select Actions. Click “New repository secret” and create a secret named DEEPSEEK_API_KEY, pasting the API key value from the DeepSeek platform dashboard.
Never hardcode API keys in workflow files or source code. For production repositories, use environment-scoped secrets to restrict key access to specific deployment environments, reducing blast radius if a secret is exposed.
Step 2: Creating the GitHub Actions Workflow File
Minimal Workflow YAML (extended with file scoping in Step 3)
Create the file .github/workflows/deepseek-code-review.yml with the following content:
name: DeepSeek Code Review
on:
pull_request:
types: [opened, synchronize]
permissions:
pull-requests: write
contents: read
jobs:
code-review:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: DeepSeek Code Review
uses: deepseek-ai/code-review-action@- COMMIT-SHA>
with:
model: deepseek-coder
language: en
max_tokens: ${{ vars.MAX_REVIEW_TOKENS || 4096 }}
review_prompt: |
You are a senior code reviewer specializing in JavaScript, React, and Node.js.
Review the following code changes for bugs, security vulnerabilities,
performance issues, and adherence to modern best practices.
Be specific and actionable in your feedback.
exclude: |
node_modules/**
dist/**
package-lock.json
yarn.lock
*.min.js
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The include: parameter for file scoping is added in Step 3. Use the configuration from Step 3 for production deployments.
Line-by-Line Breakdown
Setting on.pull_request with types opened and synchronize fires the workflow both when a PR is first created and when new commits land on an existing PR branch. Grant pull-requests: write so the action can post review comments on the PR, and contents: read so it can access repository code. Without pull-requests: write, the action fails with an HTTP 403 permission error, visible in the Actions run log.
Pinning the
uses:line to a specific commit SHA protects against supply-chain attacks on workflows with write permissions. Always pin to a verified commit SHA rather than a mutable tag like@v1.
Pinning the uses: line to a specific commit SHA protects against supply-chain attacks on workflows with write permissions. Always pin to a verified commit SHA rather than a mutable tag like @v1. Find the SHA by running curl -s https://api.github.com/repos/deepseek-ai/code-review-action/git/ref/tags/v1 | jq -r '.object.sha' and verify it against the release you have audited. The with: block passes configuration directly to the action. model selects deepseek-coder for its code-specific training. language sets the review comment output language; set to en for English. max_tokens caps response length (defaulting to 4096 tokens), balancing thoroughness against API costs. exclude accepts glob patterns to skip irrelevant files.
Setting fetch-depth: 0 on the checkout step fetches the full commit history so the action can compute the diff between the PR branch and the base branch. Removing it breaks diff extraction.
Injecting the API key from GitHub Secrets via the env block keeps credentials out of the workflow file, while GITHUB_TOKEN handles PR comment authentication. For repositories that receive PRs from forks, GITHUB_TOKEN has read-only pull-request permissions by default. The workflow includes a comment block at the top explaining this limitation and the options available. Use pull_request_target with extreme caution: never check out or run fork branch code under pull_request_target without explicit sandboxing.
Step 3: Customizing the Review Prompt for JavaScript and React
Writing an Effective System Prompt
The review prompt controls review quality more than any other setting. A well-crafted prompt steers DeepSeek toward the specific issues that matter to the team. Replace the generic prompt in the workflow with a stack-specific version:
review_prompt: |
You are a senior full-stack engineer reviewing a JavaScript/React/Node.js codebase.
Focus on the following areas:
1. React anti-patterns: missing dependency arrays in useEffect, stale closures,
unnecessary re-renders, improper key usage in lists.
2. Security: XSS risks (especially dangerouslySetInnerHTML), unsanitized user input,
hardcoded secrets or API URLs, SQL/NoSQL injection vectors in Node.js.
3. Error handling: uncaught promise rejections, missing try/catch blocks,
swallowed errors with empty catch blocks.
4. Performance: unnecessary state updates, missing memoization for expensive
computations, N+1 query patterns in API routes.
5. Style and maintainability: ESLint-equivalent issues, inconsistent naming,
overly complex functions that should be decomposed.
Categorize each issue as [BUG], [SECURITY], [PERFORMANCE], or [STYLE].
Provide a suggested fix for each issue.
This prompt structure gives DeepSeek explicit categories to work with, which makes comments more organized and actionable. These category labels help developers triage feedback faster.
Filtering Files and Setting Scope
File filtering prevents the action from wasting tokens on generated code, dependencies, or build artifacts. Use exclude and include patterns together for precise targeting:
exclude: |
node_modules/**
dist/**
build/**
coverage/**
package-lock.json
yarn.lock
pnpm-lock.yaml
*.min.js
*.map
**/*.test.js
**/*.spec.js
include: |
src/**/*.js
src/**/*.jsx
src/**/*.ts
src/**/*.tsx
server/**/*.js
api/**/*.js
Restricting include to source files in src, server, and api while excluding test files, lock files, source maps, and minified bundles keeps reviews focused, reduces API costs, and improves comment relevance.
Step 4: Testing the Workflow with a Sample Pull Request
Creating a Deliberately Flawed PR
To validate the setup, create a branch with a React component containing several intentional issues:
import React, { useState, useEffect } from 'react';
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [unusedState, setUnusedState] = useState('this is never used');
useEffect(() => {
const fetchUser = async () => {
const response = await fetch(`http://localhost:3001/api/users/${userId}`);
const data = await response.json();
setUser(data);
setLoading(false);
};
fetchUser();
}, []);
if (loading) return <div>Loading...div>;
return (
<div>
<h1>{user.name}h1>
<div dangerouslySetInnerHTML={{ __html: user.bio }} />
<p>Email: {user.email}p>
div>
);
};
export default UserProfile;
This component contains at least seven reviewable issues: a missing userId dependency in the useEffect dependency array (stale closure risk), no cleanup function for the async operation (potential state update on unmounted component), an unused unusedState variable, a hardcoded localhost API URL, missing prop validation, no error handling for the fetch call, and a direct XSS vector via dangerouslySetInnerHTML rendering unsanitized user content.
A production-quality version of this component with all issues addressed looks like this:
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import DOMPurify from 'dompurify';
const API_BASE = process.env.REACT_APP_API_BASE_URL;
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!userId || typeof userId !== 'string') return;
const controller = new AbortController();
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(
`${API_BASE}/api/users/${encodeURIComponent(userId)}`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchUser();
return () => controller.abort();
}, [userId]);
if (loading) return <div>Loading...div>;
if (error) return <div role="alert">Error: {error}div>;
if (!user) return null;
return (
<div>
<h1>{user.name}h1>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(user.bio ?? ''),
}}
/>
<p>Email: {user.email}p>
div>
);
};
UserProfile.propTypes = {
userId: PropTypes.string.isRequired,
};
export default UserProfile;
The corrected version addresses every issue: userId is in the dependency array, an AbortController handles cleanup on unmount, the fetch checks response.ok before parsing, the URL uses an environment variable (REACT_APP_API_BASE_URL) instead of hardcoded localhost, userId is validated and URI-encoded, user.bio is sanitized with DOMPurify to prevent XSS, null states are properly guarded, and PropTypes enforce the expected input shape. Install the required dependencies with npm install prop-types dompurify.
Triggering the Review
Push the branch and open a pull request. The workflow will appear in the repository’s Actions tab. Once the job completes, navigate to the PR’s “Files changed” tab to see DeepSeek’s inline comments.
Interpreting the Results
DeepSeek’s comments should appear as inline annotations on the specific lines where issues exist. Each comment will include an issue description, a category (matching the labels defined in the prompt), and a suggested fix. Completion time varies with diff size, API load, and network latency. PRs under 300 changed lines usually finish in 30 to 90 seconds. Larger diffs with many files take longer and consume proportionally more tokens.
Step 5: Advanced Configuration and Optimization
Controlling Costs and Rate Limits
Set max_tokens to a value appropriate for the team’s budget. A limit of 4096 tokens covers roughly 3,000 words of review output, enough for single-file diffs under 500 lines. Reduce to 2048 for tighter cost control at the expense of truncated feedback on large diffs. Verify current model pricing at the DeepSeek API pricing page before making cost-based model selection decisions, as pricing changes over time.
The if: github.event.pull_request.draft == false conditional (already included in the workflow above) prevents the action from running on draft PRs. Add label-based skipping for additional control.
Multi-Model and Fallback Strategies
For teams that want resilience against API outages or want to route different review types to different models, use conditional steps. Before using this pattern, create a second GitHub secret named DEEPSEEK_API_KEY_SECONDARY in repository Settings → Secrets and variables → Actions, following the same process as Step 1.
- name: DeepSeek Code Review (Primary)
id: primary-review
continue-on-error: true
uses: deepseek-ai/code-review-action@- COMMIT-SHA>
with:
model: deepseek-coder
max_tokens: ${{ vars.MAX_REVIEW_TOKENS || 4096 }}
review_prompt: |
Review for bugs, security, and performance issues.
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Notify on primary review failure
if: steps.primary-review.outcome == 'failure'
run: echo "::warning::Primary DeepSeek review failed. Attempting fallback."
- name: Fallback Review
if: steps.primary-review.outcome == 'failure'
uses: deepseek-ai/code-review-action@- COMMIT-SHA>
with:
model: deepseek-chat
max_tokens: 2048
review_prompt: |
Review for critical bugs and security issues only.
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_SECONDARY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The continue-on-error: true flag on the primary step allows the fallback to execute if the first attempt fails. A warning annotation is emitted when the primary review fails, ensuring the fallback is observable in the Actions log. This pattern also supports using a secondary API key for a different DeepSeek deployment or account.
Integrating with Existing CI/CD
Run the DeepSeek review alongside existing linting and test jobs by adding a needs: dependency:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run lint
code-review:
needs: lint
if: always() && needs.lint.result != 'skipped'
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: DeepSeek Code Review
uses: deepseek-ai/code-review-action@- COMMIT-SHA>
with:
model: deepseek-coder
language: en
max_tokens: ${{ vars.MAX_REVIEW_TOKENS || 4096 }}
review_prompt: |
Review for bugs, security, and performance issues.
exclude: |
node_modules/**
dist/**
package-lock.json
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The if: always() && needs.lint.result != 'skipped' condition ensures the AI review runs even when linting fails, while still skipping if the lint job was itself skipped. This way, PRs with lint failures still receive AI review feedback. The permissions block is included on the code-review job to ensure it works correctly if this snippet is used standalone. Combine with branch protection rules by requiring the code-review job to pass before merging. To configure this, navigate to Settings → Branches → Add branch protection rule and enable “Require status checks to pass before merging,” selecting the code-review job.
Implementation Checklist
Setup
- DeepSeek API key obtained and stored as GitHub secret (
DEEPSEEK_API_KEY) - If using fallback pattern: second key stored as
DEEPSEEK_API_KEY_SECONDARY - Workflow file created at
.github/workflows/deepseek-code-review.yml - Action reference verified against
github.com/deepseek-aior GitHub Marketplace
Configuration
- Action pinned to commit SHA for production use
- Trigger events configured (
pull_request: [opened, synchronize]) - PR write permissions granted in workflow
- Model selected (verify current model identifiers against DeepSeek API docs)
- Custom review prompt tailored to your stack (JS/React/Node.js)
- File exclusion patterns configured (node_modules, dist, locks)
- Max token limit set for cost control
Validation
- Test PR created and reviewed successfully
- Draft PR skip logic added
- Fork PR limitation documented for the team
Production Hardening
- Integrated with existing lint/test pipeline via
needs:(with awareness of skip-on-failure behavior) - Branch protection rule updated to require AI review
- Team notified and feedback loop established
Limitations and Best Practices
What DeepSeek Won’t Catch
AI code review has clear boundaries. DeepSeek cannot evaluate business logic correctness, domain-specific rules, or architectural decisions that require understanding of the broader system context. It sees only the diff and whatever surrounding file context you provide. Cross-file dependency analysis, database schema implications, and deployment-specific concerns remain firmly in the domain of human reviewers.
Best Practices for Production Use
Always pair AI review with at least one human reviewer. AI comments can generate false positives, and periodic audits of the action’s output help calibrate the system prompt over time. Iterate on the review prompt based on team feedback: if the action consistently flags non-issues, narrow its scope; if it misses patterns the team cares about, add explicit instructions. For supply-chain security, pin the action to a specific commit SHA rather than the mutable @v1 tag: uses: deepseek-ai/code-review-action@. Verify the SHA against the release you have audited. Monitor the action repository for updates, testing new versions in a non-production repository before upgrading.
Always pair AI review with at least one human reviewer. AI comments can generate false positives, and periodic audits of the action’s output help calibrate the system prompt over time.
What Comes Next
The biggest gap in this setup is cross-file analysis. DeepSeek reviews each file’s diff in isolation, so it misses issues like broken imports, interface mismatches, or cascading type errors. Closing that gap requires either expanding the review context to include dependent files or splitting the review into a two-pass workflow: one for per-file issues, another that feeds a dependency graph summary into a second prompt.
Beyond that, teams can extend the pipeline by adding Slack or Discord notifications for AI review summaries, experimenting with self-hosted DeepSeek models for air-gapped environments, or adapting the workflow for monorepo configurations with per-package review prompts. Use the implementation checklist above as a reference when onboarding new repositories.

