|
| 1 | +--- |
| 2 | +title: "GitHub Actions CI/CD" |
| 3 | +description: "Run AI-powered code review, testing, and validation in secure E2B sandboxes from your GitHub Actions workflows." |
| 4 | +icon: "gears" |
| 5 | +--- |
| 6 | + |
| 7 | +CI/CD pipelines can use AI agents to review pull requests, generate tests, and validate code changes automatically. E2B sandboxes provide the secure, isolated execution environment where these agents can safely clone repositories, run untrusted code, and report results — all triggered by [GitHub Actions](https://docs.github.com/en/actions) on every pull request. Since each run gets a fresh sandbox, malicious or buggy PR code never touches your CI runner. |
| 8 | + |
| 9 | +## How It Works |
| 10 | + |
| 11 | +The AI-powered CI/CD workflow follows this pattern: |
| 12 | + |
| 13 | +1. **A pull request is opened or updated** — GitHub Actions triggers a workflow |
| 14 | +2. **The workflow creates an E2B sandbox** — a fresh Linux environment isolated from the CI runner |
| 15 | +3. **The repository is cloned into the sandbox** — the PR branch is checked out using E2B's [git integration](/docs/sandbox/git-integration) |
| 16 | +4. **An LLM analyzes the changes** — the diff is sent to a language model (e.g., OpenAI GPT-4o, Anthropic Claude) for review or test generation |
| 17 | +5. **Tests run inside the sandbox** — the project's test suite executes in isolation, with output streamed back to the workflow |
| 18 | +6. **Results are reported** — the workflow posts findings as PR comments via the GitHub API |
| 19 | + |
| 20 | +## Install the E2B SDK |
| 21 | + |
| 22 | +The [E2B SDK](https://github.com/e2b-dev/e2b) provides sandbox creation, command execution, file operations, and git integration for your CI/CD scripts. |
| 23 | + |
| 24 | +<CodeGroup> |
| 25 | +```bash JavaScript & TypeScript |
| 26 | +npm i e2b |
| 27 | +``` |
| 28 | +```bash Python |
| 29 | +pip install e2b |
| 30 | +``` |
| 31 | +</CodeGroup> |
| 32 | + |
| 33 | +## GitHub Actions Workflow |
| 34 | + |
| 35 | +Define a workflow that triggers on pull request events and runs your AI review script. Store your `E2B_API_KEY` and LLM API key as [GitHub Actions secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) — the built-in `GITHUB_TOKEN` is available automatically. |
| 36 | + |
| 37 | +```yaml .github/workflows/ai-review.yml |
| 38 | +name: AI Code Review |
| 39 | + |
| 40 | +on: |
| 41 | + pull_request: |
| 42 | + types: [opened, synchronize] |
| 43 | + |
| 44 | +permissions: |
| 45 | + pull-requests: write |
| 46 | + |
| 47 | +jobs: |
| 48 | + ai-review: |
| 49 | + runs-on: ubuntu-latest |
| 50 | + steps: |
| 51 | + - uses: actions/checkout@v4 |
| 52 | + |
| 53 | + - name: Set up Node.js |
| 54 | + uses: actions/setup-node@v4 |
| 55 | + with: |
| 56 | + node-version: "20" |
| 57 | + |
| 58 | + - name: Install dependencies |
| 59 | + run: npm install e2b openai |
| 60 | + |
| 61 | + - name: Run AI review |
| 62 | + env: |
| 63 | + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} |
| 64 | + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} |
| 65 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 66 | + PR_REPO: ${{ github.event.pull_request.head.repo.full_name }} |
| 67 | + PR_BRANCH: ${{ github.event.pull_request.head.ref }} |
| 68 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 69 | + GITHUB_REPOSITORY: ${{ github.repository }} |
| 70 | + run: node review.mjs |
| 71 | +``` |
| 72 | +
|
| 73 | +For Python, replace the setup step with `actions/setup-python@v5`, install with `pip install e2b openai`, and run `python review.py` instead. |
| 74 | + |
| 75 | +## Core Implementation |
| 76 | + |
| 77 | +The following snippets show how to build an AI-powered code review script that runs inside the GitHub Actions workflow above. |
| 78 | + |
| 79 | +### Cloning the pull request |
| 80 | + |
| 81 | +Create a sandbox and clone the PR branch. The `GITHUB_TOKEN` provided by GitHub Actions works as the git password when paired with `x-access-token` as the username — this is the standard [GitHub Apps authentication pattern](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation). |
| 82 | + |
| 83 | +<CodeGroup> |
| 84 | +```typescript JavaScript & TypeScript |
| 85 | +import { Sandbox } from 'e2b' |
| 86 | +
|
| 87 | +// Create a sandbox with a 5-minute timeout |
| 88 | +const sandbox = await Sandbox.create({ timeoutMs: 300_000 }) |
| 89 | +
|
| 90 | +const repoUrl = `https://github.com/${process.env.PR_REPO}.git` |
| 91 | + |
| 92 | +// Clone the PR branch into the sandbox |
| 93 | +await sandbox.git.clone(repoUrl, { |
| 94 | + path: '/home/user/repo', |
| 95 | + branch: process.env.PR_BRANCH, |
| 96 | + username: 'x-access-token', |
| 97 | + password: process.env.GITHUB_TOKEN, |
| 98 | + depth: 1, |
| 99 | +}) |
| 100 | + |
| 101 | +console.log('Repository cloned successfully') |
| 102 | +``` |
| 103 | +```python Python |
| 104 | +import os |
| 105 | +from e2b import Sandbox |
| 106 | + |
| 107 | +# Create a sandbox with a 5-minute timeout |
| 108 | +sandbox = Sandbox.create(timeout=300) |
| 109 | + |
| 110 | +repo_url = f"https://github.com/{os.environ['PR_REPO']}.git" |
| 111 | + |
| 112 | +# Clone the PR branch into the sandbox |
| 113 | +sandbox.git.clone( |
| 114 | + repo_url, |
| 115 | + path="/home/user/repo", |
| 116 | + branch=os.environ["PR_BRANCH"], |
| 117 | + username="x-access-token", |
| 118 | + password=os.environ["GITHUB_TOKEN"], |
| 119 | + depth=1, |
| 120 | +) |
| 121 | + |
| 122 | +print("Repository cloned successfully") |
| 123 | +``` |
| 124 | +</CodeGroup> |
| 125 | + |
| 126 | +### Running AI code review |
| 127 | + |
| 128 | +Run `git diff` inside the sandbox to collect the changes, then send them to an LLM for analysis. Because the diff is extracted inside the sandbox, even repositories with malicious build hooks cannot affect the CI runner. |
| 129 | + |
| 130 | +<CodeGroup> |
| 131 | +```typescript JavaScript & TypeScript |
| 132 | +import { Sandbox } from 'e2b' |
| 133 | +import OpenAI from 'openai' |
| 134 | + |
| 135 | +const sandbox = await Sandbox.create({ timeoutMs: 300_000 }) |
| 136 | + |
| 137 | +// ... clone step from above |
| 138 | + |
| 139 | +// Get the diff against the base branch |
| 140 | +const diffResult = await sandbox.commands.run( |
| 141 | + 'cd /home/user/repo && git diff origin/main...HEAD' |
| 142 | +) |
| 143 | +const diff = diffResult.stdout |
| 144 | + |
| 145 | +// Send the diff to an LLM for review |
| 146 | +const openai = new OpenAI() |
| 147 | +const response = await openai.chat.completions.create({ |
| 148 | + model: 'gpt-4o', |
| 149 | + messages: [ |
| 150 | + { |
| 151 | + role: 'system', |
| 152 | + content: |
| 153 | + 'You are a senior code reviewer. Analyze the following git diff and provide a concise review with actionable feedback. Focus on bugs, security issues, and code quality.', |
| 154 | + }, |
| 155 | + { |
| 156 | + role: 'user', |
| 157 | + content: `Review this diff:\n\n${diff}`, |
| 158 | + }, |
| 159 | + ], |
| 160 | +}) |
| 161 | + |
| 162 | +const review = response.choices[0].message.content |
| 163 | +console.log('AI Review:', review) |
| 164 | +``` |
| 165 | +```python Python |
| 166 | +import os |
| 167 | +from e2b import Sandbox |
| 168 | +from openai import OpenAI |
| 169 | + |
| 170 | +sandbox = Sandbox.create(timeout=300) |
| 171 | + |
| 172 | +# ... clone step from above |
| 173 | + |
| 174 | +# Get the diff against the base branch |
| 175 | +diff_result = sandbox.commands.run( |
| 176 | + "cd /home/user/repo && git diff origin/main...HEAD" |
| 177 | +) |
| 178 | +diff = diff_result.stdout |
| 179 | + |
| 180 | +# Send the diff to an LLM for review |
| 181 | +client = OpenAI() |
| 182 | +response = client.chat.completions.create( |
| 183 | + model="gpt-4o", |
| 184 | + messages=[ |
| 185 | + { |
| 186 | + "role": "system", |
| 187 | + "content": "You are a senior code reviewer. Analyze the following git diff and provide a concise review with actionable feedback. Focus on bugs, security issues, and code quality.", |
| 188 | + }, |
| 189 | + { |
| 190 | + "role": "user", |
| 191 | + "content": f"Review this diff:\n\n{diff}", |
| 192 | + }, |
| 193 | + ], |
| 194 | +) |
| 195 | + |
| 196 | +review = response.choices[0].message.content |
| 197 | +print("AI Review:", review) |
| 198 | +``` |
| 199 | +</CodeGroup> |
| 200 | + |
| 201 | +### Running tests in the sandbox |
| 202 | + |
| 203 | +Use the sandbox to run the project's test suite in isolation, streaming output in real time to the GitHub Actions logs. This ensures that untrusted PR code executes in a sandboxed environment rather than on the CI runner itself. |
| 204 | + |
| 205 | +The `commands.run()` method throws when a command exits with a non-zero code — `CommandExitError` in JavaScript and `CommandExitException` in Python. Catch these to handle test failures gracefully. |
| 206 | + |
| 207 | +<CodeGroup> |
| 208 | +```typescript JavaScript & TypeScript |
| 209 | +import { Sandbox, CommandExitError } from 'e2b' |
| 210 | + |
| 211 | +const sandbox = await Sandbox.create({ timeoutMs: 300_000 }) |
| 212 | + |
| 213 | +// ... clone step from above |
| 214 | + |
| 215 | +// Install dependencies with streaming output |
| 216 | +await sandbox.commands.run('cd /home/user/repo && npm install', { |
| 217 | + onStdout: (data) => console.log(data), |
| 218 | + onStderr: (data) => console.error(data), |
| 219 | +}) |
| 220 | + |
| 221 | +// Run the test suite |
| 222 | +try { |
| 223 | + await sandbox.commands.run('cd /home/user/repo && npm test', { |
| 224 | + onStdout: (data) => console.log(data), |
| 225 | + onStderr: (data) => console.error(data), |
| 226 | + }) |
| 227 | + console.log('All tests passed') |
| 228 | +} catch (err) { |
| 229 | + if (err instanceof CommandExitError) { |
| 230 | + console.error('Tests failed with exit code:', err.exitCode) |
| 231 | + process.exit(1) |
| 232 | + } |
| 233 | + throw err |
| 234 | +} |
| 235 | + |
| 236 | +await sandbox.kill() |
| 237 | +``` |
| 238 | +```python Python |
| 239 | +import sys |
| 240 | +from e2b import Sandbox, CommandExitException |
| 241 | + |
| 242 | +sandbox = Sandbox.create(timeout=300) |
| 243 | + |
| 244 | +# ... clone step from above |
| 245 | + |
| 246 | +# Install dependencies with streaming output |
| 247 | +sandbox.commands.run( |
| 248 | + "cd /home/user/repo && npm install", |
| 249 | + on_stdout=lambda data: print(data), |
| 250 | + on_stderr=lambda data: print(data, file=sys.stderr), |
| 251 | +) |
| 252 | + |
| 253 | +# Run the test suite |
| 254 | +try: |
| 255 | + sandbox.commands.run( |
| 256 | + "cd /home/user/repo && npm test", |
| 257 | + on_stdout=lambda data: print(data), |
| 258 | + on_stderr=lambda data: print(data, file=sys.stderr), |
| 259 | + ) |
| 260 | + print("All tests passed") |
| 261 | +except CommandExitException as err: |
| 262 | + print(f"Tests failed with exit code: {err.exit_code}", file=sys.stderr) |
| 263 | + sys.exit(1) |
| 264 | + |
| 265 | +sandbox.kill() |
| 266 | +``` |
| 267 | +</CodeGroup> |
| 268 | + |
| 269 | +To post the AI review as a PR comment, use the GitHub REST API with the `GITHUB_TOKEN`. See [Using the GitHub CLI in workflows](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-github-cli-in-workflows) for more ways to interact with GitHub from your workflow scripts. |
| 270 | + |
| 271 | +## Related Guides |
| 272 | + |
| 273 | +<CardGroup cols={3}> |
| 274 | + <Card title="Git Integration" icon="code-branch" href="/docs/sandbox/git-integration"> |
| 275 | + Clone repos, manage branches, and push changes from sandboxes |
| 276 | + </Card> |
| 277 | + <Card title="Connect LLMs" icon="brain" href="/docs/quickstart/connect-llms"> |
| 278 | + Integrate AI models with sandboxes using tool calling |
| 279 | + </Card> |
| 280 | + <Card title="Custom Templates" icon="cube" href="/docs/template/quickstart"> |
| 281 | + Build reproducible sandbox environments for your pipelines |
| 282 | + </Card> |
| 283 | +</CardGroup> |
0 commit comments